Java学习笔记——面向对象

Java也不是学得第一个语言了,这里就只记录一些学习过程中发现的之前的没有见过的事情吧

CC++golang对照着学习。

语句

循环 for each

public class Main {
    public static void main(String[] args) {
        int[] ns = { 1, 4, 9, 16, 25 };
        for (int n : ns) {
            System.out.println(n);
        }
    }
}
  • 更简单地遍历数组
  • 但是无法指定遍历顺序
  • 也无法获取数组的索引
  • 能够遍历所有“可迭代”的数据类型,包括List、Map等

方法(method)

构造方法

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Person(String name) {
        this(name, 18); // 调用另一个构造方法Person(String, int)
    }

    public Person() {
        this("Unnamed"); // 调用另一个构造方法Person(String)
    }
}

一个构造方法可以调用其他构造方法,这样做的目的是便于代码复用。

创建实例时初始化顺序为,先初始化字段,再执行构造方法。

默认构造方法由编译器生成,没有参数,没有执行语句,如果已有自定义,需要重新定义一次默认构造方法。

重载方法

String类提供了多个重载方法indexOf(),可以查找子串:

  • int indexOf(int ch):根据字符的Unicode码查找;
  • int indexOf(String str):根据字符串查找;
  • int indexOf(int ch, int fromIndex):根据字符查找,但指定起始位置;
  • int indexOf(String str, int fromIndex)根据字符串查找,但指定起始位置。

方法名相同,但各自的参数不同,称为方法重载(Overload

注意:方法重载的返回值类型通常都是相同的。

Arrays类

import java.util.Arrays;
// ns为数组名

// 将一维数组转为字符串
Arrays.toString(ns)
    
// 将多维数组转为字符串
Arrays.deepToString(ns)
    
// 数组内容升序排列
Arrays.toString(ns)

继承

protect关键字

class Person {
    protected String name;
    protected int age;
}

class Student extends Person {
    public String hello() {
        return "Hello, " + name; // OK!
    }
}

子类无法访问父类的private字段或者private方法

protected关键字可以把字段和方法的访问权限控制在继承树内部,一个protected字段和方法可以被其子类,以及子类的子类所访问

super关键字

public class Main {
    public static void main(String[] args) {
        Student s = new Student("Xiao Ming", 12, 89);
    }
}

class Person {
    protected String name;
    protected int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

class Student extends Person {
    protected int score;
    
    public Student(String name, int age, int score) {
        super(name, age); // 调用父类的构造方法Person(String, int)
        this.score = score;
    }
}

在Java中,任何class的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句super();

如果父类没有默认的构造方法,子类就必须显式调用super()并给出参数以便让编译器定位到父类的一个合适的构造方法。

子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。

转型

Java允许向上转型,抛弃一部分内容,把一个子类型安全地变为更加抽象的父类型。

但是向下转型可能会失败,不能把父类变为子类,因为子类功能比父类多,多的功能无法凭空变出来,但是可以把实际是子类的父类向下转型为“子类”

向下转型前要通过instanceof运算符进行判断,instanceof实际上判断一个变量所指向的实例是否是指定类型,或者这个类型的子类。如果一个引用变量为null,那么对任何instanceof的判断都为false

Person p = new Student();
if (p instanceof Student) {
    // 只有判断成功才会向下转型:
    Student s = (Student) p; // 一定会成功
}

区分组合与继承

子类和父类的关系是is,has关系不能用继承。

多态

覆写Override

在继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为覆写(Override)。

Override和Overload不同的是,如果方法签名如果不同,就是Overload,Overload方法是一个新方法;如果方法签名相同,并且返回值也相同,就是Override。方法名相同,方法参数相同,但方法返回值不同,也是不同的方法。在Java程序中,出现这种情况,编译器会报错。

加上@Override可以让编译器帮助检查是否进行了正确的覆写。希望进行覆写,但是不小心写错了方法签名,编译器会报错。

public class Main {
    public static void main(String[] args) {
    }
}

class Person {
    public void run() {}
}

public class Student extends Person {
    @Override // Compile error!
    public void run(String s) {}
}

多态Polymorphic

运行期才能动态决定调用的子类方法。对某个类型调用某个方法,执行的实际方法可能是某个子类的覆写方法。

// 多态例子
public class Main {
    public static void main(String[] args) {
        // 给一个有普通收入、工资收入和享受国务院特殊津贴的小伙伴算税:
        Income[] incomes = new Income[] {
            new Income(3000),
            new Salary(7500),
            new StateCouncilSpecialAllowance(15000)
        };
        System.out.println(totalTax(incomes));
    }

    public static double totalTax(Income... incomes) {
        double total = 0;
        for (Income income: incomes) {
            total = total + income.getTax();
        }
        return total;
    }
}

class Income {
    protected double income;

    public Income(double income) {
        this.income = income;
    }

    public double getTax() {
        return income * 0.1; // 税率10%
    }
}

class Salary extends Income {
    public Salary(double income) {
        super(income);
    }

    @Override
    public double getTax() {
        if (income <= 5000) {
            return 0;
        }
        return (income - 5000) * 0.2;
    }
}

class StateCouncilSpecialAllowance extends Income {
    public StateCouncilSpecialAllowance(double income) {
        super(income);
    }

    @Override
    public double getTax() {
        return 0;
    }
}

调用super

// 在子类的覆写方法中,如果要调用父类的被覆写的方法,可以通过super来调用。
class Person {
    protected String name;
    public String hello() {
        return "Hello, " + name;
    }
}

Student extends Person {
    @Override
    public String hello() {
        // 调用父类的hello()方法:
        return super.hello() + "!";
    }
}

抽象类

abstract关键字

把一个方法声明为abstract,表示它是一个抽象方法,本身没有实现任何方法语句。由于这个抽象方法本身是无法执行的,所以,抽象方法所在的类也无法被实例化。必须把类本身也声明为abstract,才能正确编译它

public class Main {
    public static void main(String[] args) {
        Person p = new Student();
        p.run();
    }
}

abstract class Person {
    public abstract void run();
}

class Student extends Person {
    @Override
    public void run() {
        System.out.println("Student.run");
    }
}

由抽象类的概念,引申出面向抽象编程的概念,上层代码(抽象类)只定义规范(如run方法必须覆写),具体的业务逻辑由下层代码(具体的类)实现,调用者不必关心。

接口

接口相比于抽象类没有字段,所有方法全部都是抽象方法。接口是比抽象类还要抽象的纯抽象接口。接口定义的所有方法默认都是public abstract的,所以这两个修饰符不需要写出来(写不写效果都一样)。

implement关键字

// 具体的类通过implement关键字实现interface。
class Student implements Person {
    private String name;

    public Student(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println(this.name + " run");
    }

    @Override
    public String getName() {
        return this.name;
    }
}

接口继承与default方法

interface继承自interface使用extends,它相当于扩展了接口的方法。接口可以继承自多个接口,这与类的继承不同。

interface Person {
    String getName();
     
   	// 实现类可以不必覆写default方法
    default void run() {
        System.out.println(getName() + " run");
    }
}

class Student implements Person {
    private String name;

    public Student(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
}

default方法和抽象类的普通方法是有所不同的。因为interface没有字段,default方法无法访问字段,而抽象类的普通方法可以访问实例字段。

接口与抽象类比较

abstract classinterface
继承只能extends一个class可以implements多个interface
字段可以定义实例字段不能定义实例字段
抽象方法可以定义抽象方法可以定义抽象方法
非抽象方法可以定义非抽象方法可以定义default方法

静态字段与静态方法

实例字段

通常从类中定义的字段称为实例字段。每个实例都有独立的字段,各个实例的同名字段互不影响

静态字段

每个实例中的静态字段共享一个空间。静态字段不属于实例,是另存与实例之外的字段。一般不使用实例访问静态字段,而是通过类名访问静态字段

静态方法

调用静态方法不需要实例变量,通过类名就可以调用。类似于其他编程语言的函数。在静态方法内部无法访问this变量,也无法访问其他实例字段只能访问静态字段。常用于辅助方法,例如Java程序入口的main()

public class Main {
    public static void main(String[] args) {
        Person.setNumber(99);
        System.out.println(Person.number);
    }
}

class Person {
    public static int number;

    public static void setNumber(int value) {
        number = value;
    }
}

接口静态字段

接口可以有静态字段,但是静态字段必须为final类型。因为接口的字段只能是public static final类型,所以简写也可以。编译器会把通过实例调用的静态方法、静态字段,改写为通过类名调用的。编译器也会把接口中省略的静态子段修饰符自己补上。

public interface Person {
    // 编译器会自动加上public statc final:
    int MALE = 1;
    int FEMALE = 2;
}

包名

包是Java中的命名空间,解决名字冲突。需要在定义class的时候第一行申明这个class属于哪个包。没有定义包名的class使用的是默认包,容易引起名字冲突。JVM执行的时候只区分完整类名:包名.类名.

包名也可以是多层结构,用“ . ”分隔开,推荐使用倒置的域名作为包名。

在电脑中存储的时候,Java文件对应的目录层次要和包的层次保持一致。编译后的class文件也是如此。IDE会帮助完成这项工作。

同一个包中的类,可以相互访问包作用域下的字段和方法,即不用public / protected / private修饰的字段和方法

import关键字

导入可以是完整包名,也可以通过 * 一次导入该包下所有类(不建议)

还可以通过import static导入包中类中的静态字段与静态方法(少用)

编译器查找包逻辑

  • 如果是完整类名,就直接根据完整类名查找这个class
  • 如果是简单类名,按下面的顺序依次查找
    • 查找当前package是否存在这个class
    • 查找import的包是否包含这个class
    • 查找java.lang包是否包含这个class
  • 如果无法确定,编译报错

默认自动import当前package的其他class;默认自动import java.lang.*。自动导入的是java.lang包,但类似java.lang.reflect这些子包仍需要手动导入。如果有两个class名称相同,例如,mr.jun.Arraysjava.util.Arrays,那么只能import其中一个,另一个必须写完整类名。

作用域

public

  • 定义为public的classinterface可以被其他任何类访问
  • 定义为publicfieldmethod可以被其他类访问,前提是首先有访问class的权限
  • 如果不确定是否需要public,就不声明为public,即尽可能少地暴露对外的字段和方法
  • 一个.java文件只能包含一个public类,但可以包含多个非public

private

  • 定义为private的field、method无法被其他类访问
  • private访问权限被限定在class的内部,而且与方法声明顺序无关

推荐把private方法放到后面,因为public方法定义了类对外提供的功能,阅读代码的时候,应该先关注public方法。

// 如果一个类内部还定义了嵌套类(nested class),那么,嵌套类拥有访问private的权限
public class Main {
    public static void main(String[] args) {
        Inner i = new Inner();
        i.hi();
    }

    // private方法:
    private static void hello() {
        System.out.println("private hello!");
    }

    // 静态内部类:
    static class Inner {
        public void hi() {
            Main.hello();
        }
    }
}

protected

  • protected作用于继承关系。定义为protected的字段和方法可以被子类访问,以及子类的子类

package

  • 包作用域是指一个类允许访问同一个package的没有public、private修饰的class,以及没有public、protected、private修饰的字段和方法
  • 只要在同一个包,就可以访问package权限的class、field和method
  • 注意,包名必须完全一致,包没有父子关系,com.apachecom.apache.abc是不同的包
  • 把方法定义为package权限有助于测试,因为测试类和被测试类只要位于同一个package,测试代码就可以访问被测试类的package权限方法。

final

  • 用final修饰class可以阻止被继承
  • 用final修饰method可以阻止被子类覆写
  • 用final修饰field可以阻止被重新赋值
  • 用final修饰局部变量可以阻止被重新赋值

classpath与jar

  • JVM通过环境变量classpath决定搜索class的路径和顺序
  • 不推荐设置系统环境变量classpath,始终建议通过-cp命令传入
  • jar包相当于目录,可以包含很多.class文件,方便下载和使用
  • MANIFEST.MF文件可以提供jar包的信息,如Main-Class,这样可以直接运行jar包。
  • JVM自带的标准库rt.jar不要写到classpath中,写了反而会干扰JVM的正常运行。

模块

为了解决jar包之间的依赖问题,Java9后引入了模块的概念。.jmod文件每一个都是一个模块,模块名就是文件名。例如:模块java.base对应的文件就是java.base.jmod。模块之间的依赖关系已经被写入到模块内的module-info.class文件了。所有的模块都直接或间接地依赖java.base模块,只有java.base模块不依赖任何模块,它可以被看作是“根模块”,好比所有的类都是从Object直接或间接继承而来。

把一堆class封装为jar仅仅是一个打包的过程,而把一堆class封装为模块则不但需要打包,还需要写入依赖关系,并且还可以包含二进制代码(通常是JNI扩展)。此外,模块支持多版本,即在同一个模块中可以为不同的JVM提供不同的版本。

其他具体内容参见廖雪峰教程——模块