03 面向对象编程
方法
this变量:在方法内部,可以使用一个隐含的
this变量,它指向当前实例。this可用于规避命名冲突。class Point { private double x; private double y; /** * Move the point by `x` and `y` along the x and y axis */ public void translate(double x, double y) { this.x += x; this.y += y; } }可变参数
通过
type... name可以让一个方法接受一个名为names、类型为type[]的数组。class Group { private String[] names; public void setNames(String... names) { this.names = names; } }我们可以向
Group.setNames传入任意数量的StringGroup g = new Group(); g.setNames("Foo") g.setNames("Foo", "Bar", "Baz") g.setNames()值得注意的是,无参数时,
names会是一个空数组,而非null。g.setNames()后,Arrays.equals(names, new String[]{})传参与引用
Java的变量有两种类型:基本类型和引用类型
基本类型
byte, short, int, long, float, double, char, boolean
基本类型变量的值就是实际的值,比如
int a = 3,那么a的值就是3。引用类型
类、接口、数组、
null引用类型变量的值是引用对象在堆内存中的地址
确切地说,引用类型变量的值是对堆中对象的引用,只要能一一对应到堆的地址就可以,不一定真的是地址本身。如果想的话,你也可以把
地址 + 0xdeadbeaf作为引用变量的值,然后魔改一个JVM来转换到真实的地址,这些操作理论上仍然符合标准。另外,引用类型变量和C++中的指针有所不同,引用只能指向堆中的合法对象(类似一个地址永远合法的指针),或者不指向任何对象(值为
null)。方便起见,我们就直接说引用类型变量的值是引用对象的地址了。
Java传参永远是值传递,但引用类型变量的值是引用对象在堆内存上的地址,所以传入引用类型变量时,传入的虽然是引用类型变量的值,但却是引用对象的地址。这就导致了以下现象
class Team { List<String> members; Team(List<String> members) { this.members = members; } void printMembers() { System.out.println(members); } } public class Main { public static void main(String[] args) { List<String> sharedList = new ArrayList<>(); sharedList.add("Alice"); Team team = new Team(sharedList); team.printMembers(); // [Alice] // 外部代码修改了 sharedList sharedList.add("Bob"); // 类内部的成员变量也“变”了 team.printMembers(); // [Alice, Bob] } }
构造方法
创建实例时,通过new操作符会调用类的构造方法,用以初始化实例。
任何类都有一个默认的构造方法
class Person {
    public Person() {
    }
}
int字段默认为0,boolean字段默认为false,引用类型字段默认为null
如果自定义了一个构造方法,那么编译器不会生成默认的构造方法
class Person {
    public Person(String name) {
      this.name = name;
    }
}
public class Main {
    public static void main(String[] args) {
        Person person = new Person(); // 报错
    }
}
我们可以定义多个不同签名的构造方法
class Person {
    private String name;
    private int age;
    public Person() {
    }
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
public class Main {
    public static void main(String[] args) {
        Person p1 = new Person("Xiao Ming", 15); // 既可以调用带参数的构造方法
        Person p2 = new Person(); // 也可以调用无参数构造方法
    }
}
方法重载
我们可以重载(overload)方法,即“定义一系列同名但不同参数的方法”,如
class Vector {
  private double x;
  private double y;
  public Vector(double x, double y) {
    this.x = x;
    this.y = y;
  }
  public void increment(Vector other) {
    this.x += other.x;
    this.y += other.y;
  }
  public void increment(double x, double y) {
    this.x += x;
    this.y += y;
  }
}
public class Main {
    public static void main(String[] args) {
        Vector v1 = new Vector(3, 4);
        Vector v2 = new Vector(2, 2);
        v1.increment(v2);       // v1 = (5, 6)
        v1.increment(-2, -2);   // v1 = (3, 4)
    }
}
继承
class Person {
    private String name;
    private int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
class Student extends Person {
    // 不要重复name和age字段/方法,
    // 只需要定义新增score字段/方法:
    private int score;
}
class Student extends Person表示Student类继承Person类。一个子类只能继承一个父类,Java只支持单继承。所有类的都有一个名为Object的父类。
多继承与菱形问题
不支持多继承是为了避免菱形问题。允许多继承的情况下,假如有父类A,类B和C继承A,类D又继承B和C。如果A有方法run(),B和C都可以覆写这一方法,编译器无法判断D应该继承谁的run()方法。
  A
 / \
B   C
 \ /
  D
protected
子类无法访问父类的private字段,但可以访问父类的protected字段。
super
super表示父类。我们可以使用super.fieldName,在子类中访问父类的字段。
子类的构造方法必须在第一行调用父类的构造方法,如果没有明确写出,编译器会自动加一句super()。也就是说,这段代码
public Person(String name, int age, int score) {
  this.score = score;
}
会被转换成
public Person(String name, int age, int score) {
  super();
  this.score = score;
}
由于父类Person没有签名为public Person()的构造方法,Person在实例化时会报错!
正确的做法应该是
public Person(String name, int age, int score) {
  super(name, age);
  this.score = score;
}
阻止继承
final关键词可以让一个类不能被其他类继承
public final class Rect extends Shape {...}
Java 15开始,可以使用sealed和permits定义允许用于继承的子类
public sealed class Shape permits Rect, Circle, Triangle {...}
向上转型(upcasting)与向下转型(downcasting)
子类的实例可以被转换成父类,这叫向上转型;父类的实例不可以被转换成子类,这叫向下转型。这是因为子类比父类具有更多的方法和字段,父类能做的子类一定都能做。
Person p = new Student();   // 可以,Student实例会被转换成Person类型
Student s = new Person();   // 不可以
我们可以使用instanceof关键词来判断一个变量是否是某个类型,或者某个类型的子类
Person p = new Person();
Student s = new Student();
p instance of Person  // true
p instance of Student // false
s instance of Student // true
s instance of Person  // true
Java 14后,我们也可以强制转型
if (obj instance of String) {
    Strings s = (String) obj;
}
多态(Polymorphism)
子类可以覆写父类的方法
class Person {
    public void run() {
        System.out.println("Person.run");
    }
}
class Student extends Person {
    @Override
    public void run() {
        System.out.println("Student.run");
    }
}
两个方法的签名必须一致,否则就只是在重载(Overload),而非覆写。
@Override
我们可以使用@Override的注解(annotation),明确表示一个方法在覆写父类的方法。此时编译器会进行检查,如果没有覆写就会报错。但这不是必须的。
多态
值得注意的是,即使向下转型,调用run时还是会调用被覆写的子类的run
Person p = new Student();
p.run()   // "Student.run"
这种行为被成为多态,即“编译时仅保证签名确定,运行时动态决定调用的方法”:
- 父类被覆写的方法可以使用
super调用,如 
class Student extends Person {
    @Override
    public void run() {
        System.out.println("Student.run");
        System.out.println("then,");
        super.run();    // "Person.run"
    }
}
覆写Object的方法
Object是所有类的父类。它有几个重要的方法可被用于覆写
toString()``:把实例输出为String`;equals():判断两个实例是否逻辑相等;hashCode():计算一个实例的哈希值。
例如
class Person {
    ...
    // 显示更有意义的字符串:
    @Override
    public String toString() {
        return "Person:name=" + name;
    }
    // 比较是否相等:
    @Override
    public boolean equals(Object o) {
        // 当且仅当o为Person类型:
        if (o instanceof Person) {
            Person p = (Person) o;
            // 并且name字段相同时,返回true:
            return this.name.equals(p.name);
        }
        return false;
    }
    // 用 Person.name 的 hash 作为 Person 的 hash 
    @Override
    public int hashCode() {
        return this.name.hashCode();
    }
}
final
final关键词用处很多
用于方法,确保一个方法不会被覆写。
用于类,确保一个类不能被继承
用于字段,确保一个字段只能在声明或构造时被初始化一次,之后就再不能被修改
抽象类
抽象类无法被实例化,仅用于被子类继承。
抽象类可以包含抽象方法。抽象方法无法被实现,继承抽象类的子类必须覆写抽象方法。
abstract class Person {
  public String name;
  public abstract void run();
  public say_my_name() {
    System.out.printf("My name is %s\n", this.name);
  }
}
class Student extends Person {
  public Student(String name) {
    this.name = name;
  }
  @Override
  public void run() {
    System.out.println("A student is running...");
  }
}
class Employee extends Person {
  public Employee(String name) {
    this.name = name;
  }
  @Override
  public void run() {
    System.out.println("An employee is running...");
  }
}
抽象类和其子类也可以向上转型,例如
void everyone_run(Person[] persons) {
  for (Person person : persons) {
    person.run();
  }
}
void main() {
  Person[] persons = new Person[] {
    new Student("Alice"),
    new Employee("Bob"),
  };
  everyone_run(persons);
}
可以发现,方法everyone_run只需要处理一个Person的数组,我们只需要知道处理的对象都有一个run()方法,而不需要关心具体是哪个子类继承了Person,以及run()是怎么被实现的。这一操作被称为面向抽象编程。其特征是
上层代码只定义规范(例如
abstract class Person);不需要子类就可以实现业务逻辑(正常编译);
具体的业务逻辑由不同的子类实现,调用者并不关心。
接口
现代编程会更倾向于数据与逻辑分离。抽象类混杂了字段、抽象方法、具体方法(有实现的方法),同时定义了数据与行为,不太好。
对比抽象类,接口仅定义了一系列行为。
interface Person {
  void run();
}
interface Hello {
  // 接口也可以定义抽象方法的默认实现,避免重复代码
  default public hello() {
    System.out.println("Hello!");
  }
}
class Student implements Person {
  public String name;
  public Student(String name) {
    this.name = name;
  }
  @Override
  public void run() {
    System.out.println("A student is running...");
  }
}
class Employee implements Person, Hello {
  public String name;
  public Employee(String name) {
    this.name = name;
  }
  @Override
  public void run() {
    System.out.println("An employee is running...");
  }
}
接口仅定义一系列抽象方法。接口可以给出抽象方法的具体实现
接口也可以继承别的接口
interface Creature {
    void live();
}
interface Person extends Creature {
    void run();
    String getName();
}
一个类可以实现多个接口。菱形问题不适用于实现多个接口的情况,因为接口不会给出方法的具体实现,只有实现接口的类才会给出具体实现。
静态字段与静态方法
我们可以在一个类中定义静态字段和静态方法,调用时通过类名调用。静态字段和静态方法常用于定义辅助方法,如Array.sort(),Math.PI。
接口也可以定义静态字段(但没有静态方法)。接口的静态字段必须是public static final
public interface Person {
    public static final int MALE = 1;
    public static final int FEMALE = 2;
}
可以简写成
public interface Person {
    // 编译器会自动加上public static final:
    int MALE = 1;
    int FEMALE = 2;
}
常用类
String和编码
StringBuilder和StringJoiner
包装类型
Integer包装int,使我们可以赋值null给一个“整数”。