以下笔记重点参考了如下链接:
1. 方法
1.1 定义class
class Person {
public String name;
public int age;
}
一个class
可以包含多个字段(field
),字段用来描述一个类的特征。上面的Person
类,我们定义了两个字段,分别为name
和age
;
1.2 创建实例
Person ming = new Person();
new Person()
是创建Person
实例。
1.3 私有变量
直接把field
用public
暴露给外部可能会破坏封装性,也就是外界可以直接随意赋值。
Person ming = new Person();
ming.name = "Xiao Ming";
ming.age = -99; // age设置为负数
将age
设为负数,这就不符逻辑。我们可以通过private
来将字段设为私有的,那么实例就无法直接修改了,也无法访问。
class Person {
private String name;
private int age;
}
1.4 修改与访问
虽然private
变量无法直接访问,但是我们可以在类
的内部通过函数方法来进行修改或者访问:
class Person {
private String name;
private int age;
// 通过方法来间接修改
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
if (age < 0 || age > 100) {
throw new IllegalArgumentException("invalid age value");
}
this.age = age;
}
// 通过方法来间接访问
public int getAge() {
return this.age;
}
public String getName() {
return this.name;
}
}
public class Main {
public static void main(String[] args) {
Person ming = new Person();
ming.setName("Xiao Ming"); // 设置name
ming.setAge(12); // 设置age
System.out.println(ming.getName() + ", " + ming.getAge());
}
}
- 请注意上述类的方法是
public
的,那么对应也有private
的方法;此时也无法直接访问方法。
1.4 this变量
在方法内部,可以使用一个隐含的变量this
,它始终指向当前实例。
class Person {
private String name;
public String getName() {
return name; // 相当于this.name
}
}
但是,如果有局部变量和字段重名,那么局部变量优先级更高,就必须加上this
。
class Person {
private String name;
public void setName(String name) {
this.name = name; // 前面的this不可少,少了就变成局部变量name了
}
}
1.5 可变参数
可变参数用类型...
定义,可变参数相当于数组类型:
class Group {
private String[] names;
public void setNames(String... names) {
this.names = names;
}
}
上面的setNames()
就定义了一个可变参数。调用时,可以这么写:
Group g = new Group();
g.setNames("Xiao Ming", "Xiao Hong", "Xiao Jun"); // 传入3个String
g.setNames("Xiao Ming", "Xiao Hong"); // 传入2个String
g.setNames("Xiao Ming"); // 传入1个String
g.setNames(); // 传入0个String
当然,可以把可变参数改写为String[]
类型:
class Group { private String[] names; public void setNames(String[] names) { this.names = names; }}
但是,调用方需要自己先构造String[]
,比较麻烦。例如:
Group g = new Group();g.setNames(new String[] {"Xiao Ming", "Xiao Hong", "Xiao Jun"}); // 传入1个String[]
1.6 参数绑定
- 基本类型参数的传递,是调用方值的复制。双方各自的后续修改,互不影响。
- 引用类型参数的传递,调用方的变量,和接收方的参数变量,指向的是同一个对象。比如传入数组,但是传入
string
类型的字符却不会。
public class Main {
public static void main(String[] args) {
Person p = new Person();
String[] fullname = new String[] { "Homer", "Simpson" };
p.setName(fullname); // 传入fullname数组
System.out.println(p.getName()); // "Homer Simpson"
fullname[0] = "Bart";
System.out.println(p.getName());
}
}
class Person {
private String[] name;
public String getName() {
return this.name[0] + " " + this.name[1];
}
public void setName(String[] name) {
this.name = name;
}
}
/*
输出:
Homer Simpson
Bart Simpson
*/
2. 构造方法
2.1 默认构造方法
我们可以通过构造方法,在创建实例的时候,就初始化实例。
public class Main {
public static void main(String[] args) {
Person p = new Person("Xiao Ming", 15);
System.out.println(p.getName());
System.out.println(p.getAge());
}
}
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
}
调用构造方法,必须用new
操作符。如果一个类没有定义构造方法,编译器会自动为我们生成一个默认构造方法,它没有参数,也没有执行语句,类似于:
class Person {
public Person() {
}
}
- 如果我们自定义了一个构造方法,那么,编译器就不再自动创建默认构造方法。
- 没有在构造方法中初始化字段时,引用类型的字段默认是
null
,数值类型的字段用默认值,int
类型默认值是0
,布尔类型默认值是false
。
2.2 多构造方法
可以定义多个构造方法,在通过new
操作符调用的时候,编译器通过构造方法的参数数量、位置和类型自动区分:
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 = name;
this.age = 12;
}
public Person() {
}
}
如果调用new Person("Xiao Ming", 20);
,会自动匹配到构造方法public Person(String, int)
。
一个构造方法可以调用其他构造方法,这样做的目的是便于代码复用。调用其他构造方法的语法是this(…)
:
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)
}
}
3. 方法重载
在一个类中,我们可以定义多个方法,它们的功能都是类似的,只有参数有所不同。例如:
class Hello {
public void hello() {
System.out.println("Hello, world!");
}
public void hello(String name) {
System.out.println("Hello, " + name + "!");
}
public void hello(String name, int age) {
if (age < 18) {
System.out.println("Hi, " + name + "!");
} else {
System.out.println("Hello, " + name + "!");
}
}
}
这种方法名相同,但各自的参数不同,称为方法重载(Overload
)。注意:方法重载的返回值类型通常都是相同的。
4. 继承
4.1 extends
java使用extends
关键字来实现继承:
class Person {
private String name;
private int age;
public String getName() {...}
public void setName(String name) {...}
public int getAge() {...}
public void setAge(int age) {...}
}
class Student extends Person {
// 不要重复name和age字段/方法,
// 只需要定义新增score字段/方法:
private int score;
public int getScore() { … }
public void setScore(int score) { … }
}
- 子类自动获得了父类的所有字段,严禁定义与父类重名的字段。
- 在OOP的术语中,我们把
Person
称为超类(super class),父类(parent class),基类(base class),把Student
称为子类(subclass),扩展类(extended class)。 - 任何类,除了
Object
,都会继承自某个类。类Person
看上去没有继承,实际继承的是object
。
4.2 protected
-
继承有个特点,就是子类无法访问父类的
private
字段或者private
方法。 -
为了让子类可以访问父类的字段,我们需要把
private
改为protected
。用protected
修饰的字段可以被子类访问:
class Person {
protected String name;
protected int age;
}
class Student extends Person {
public String hello() {
return "Hello, " + name; // OK!
}
}
因此,protected
关键字可以把字段和方法的访问权限控制在继承树内部,一个protected
字段和方法可以被其子类,以及子类的子类所访问
4.3 super
super
关键字表示父类(超类)。子类引用父类的字段时,可以用super.fieldName
。例如:
class Student extends Person {
public String hello() {
return "Hello, " + super.name;
}
}
实际上,这里使用super.name
,或者this.name
,或者name
,效果都是一样的。编译器会自动定位到父类的name
字段。但是,在某些时候,就必须使用super
:
-
任何
class
的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动
加一句super();
-
如果父类重写了构造函数,子类的构造函数第一行就要用
super()
调用父类的构造函数:
class Student extends Person {
protected int score;
public Student(String name, int age, int score) {
super(name, age); // 调用父类的构造方法Person(String, int)
this.score = score;
}
}
4.4 final
正常情况下,只要某个class没有final
修饰符,那么任何类都可以从该class继承。
从Java 15开始,允许使用sealed
修饰class,并通过permits
明确写出能够从该class继承的子类名称。
例如,定义一个Shape
类:
public sealed class Shape permits Rect, Circle, Triangle {
...
}
上述Shape
类就是一个sealed
类,它只允许指定的3个类继承
它。
4.5 向上转型
如果Student
是从Person
继承下来的,那么,一个引用类型为Person
的变量,能否指向Student
类型的实例?
Person p = new Student(); // ???
测试一下就可以发现,这种指向是允许的!
这是因为Student
继承自Person
,因此,它拥有Person
的全部功能。这种把一个子类类型安全地变为父类类型的赋值,被称为向上转型(upcasting)。
向上转型实际上是把一个子类型安全地变为更加抽象的父类型:
Student s = new Student();
Person p = s; // upcasting, ok
Object o1 = p; // upcasting, ok
Object o2 = s; // upcasting, ok
注意到继承树是Student > Person > Object
,所以,可以把Student
类型转型为Person
,或者更高层次的Object
。
4.5 向下转型
和向上转型相反,如果把一个父类类型强制转型为子类类型,就是向下转型(downcasting)。例如:
Person p1 = new Student(); // upcasting, ok
Person p2 = new Person();
Student s1 = (Student) p1; // ok
Student s2 = (Student) p2; // runtime error! ClassCastException!
向下转型很可能会失败。失败的时候,Java虚拟机会报ClassCastException
。
为了避免向下转型出错,Java提供了instanceof
操作符,可以先判断一个实例究竟是不是某种类型:
Person p = new Person();
System.out.println(p instanceof Person); // true
System.out.println(p instanceof Student); // false
Student s = new Student();
System.out.println(s instanceof Person); // true
System.out.println(s instanceof Student); // true
Student n = null;
System.out.println(n instanceof Student); // false
instanceof
实际上判断一个变量所指向的实例是否是指定类型,或者这个类型的子类。如果一个引用变量为null
,那么对任何instanceof
的判断都为false
。
利用instanceof
,在向下转型前可以先判断:
Person p = new Student();
if (p instanceof Student) {
// 只有判断成功才会向下转型:
Student s = (Student) p; // 一定会成功
}
5. 多态
5.1 覆写
在继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为覆写(Override)。
例如,在Person
类中,我们定义了run()
方法:
class Person {
public void run() {
System.out.println("Person.run");
}
}
在子类Student
中,覆写这个run()
方法:
class Student extends Person {
@Override
public void run() {
System.out.println("Student.run");
}
}
Override和Overload不同的是,如果方法签名不同,就是Overload,Overload方法是一个新方法;如果方法签名相同,并且返回值也相同,就是Override
。
-
方法声明的两个组件构成了方法签名 - 方法的名称和参数类型。
-
方法名相同,方法参数相同,但方法返回值不同,也是不同的方法。
5.2 Override
加上@Override
可以让编译器帮助检查是否进行了正确的覆写。希望进行覆写,但是不小心写错了方法签名,编译器会报错。但是@Override
不是必需的。
在上一节中,我们已经知道,引用变量的声明类型可能与其实际类型不符,例如:
Person p = new Student();
现在,我们考虑一种情况,如果子类覆写了父类的方法:
// override
5.3 多态
如果,一个实际类型为Student
,引用类型为Person
的变量,调用其run()
方法,调用的是Person
还是Student
的run()
方法?实际上调用的方法是Student
的run()
方法。
结论:Java的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。这个非常重要的特性在面向对象编程中称之为多态。
例:
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;
}
}
观察totalTax()
方法:利用多态,totalTax()
方法只需要和Income
打交道,它完全不需要知道Salary
和StateCouncilSpecialAllowance
的存在,就可以正确计算出总的税。
5.4 覆写Object方法
因为所有的class
最终都继承自Object
,而Object
定义了几个重要的方法:
toString()
:把instance输出为String
;equals()
:判断两个instance是否逻辑相等;hashCode()
:计算一个instance的哈希值。
在必要的情况下,我们可以覆写Object
的这几个方法。
5.5 调用super
在子类的覆写方法中,如果要调用父类的被覆写的方法,可以通过super
来调用。例如:
class Person {
protected String name;
public String hello() {
return "Hello, " + name;
}
}
Student extends Person {
@Override
public String hello() {
// 调用父类的hello()方法:
return super.hello() + "!";
}
}
5.6 final
继承可以允许子类覆写父类的方法。如果一个父类不允许子类对它的某个方法进行覆写,可以把该方法标记为final
。用final
修饰的方法不能被Override
:
class Person {
protected String name;
public final String hello() {
return "Hello, " + name;
}
}
Student extends Person {
// compile error: 不允许覆写
@Override
public String hello() {
}
}
如果一个类不希望任何其他类继承自它,那么可以把这个类本身标记为final
。用final
修饰的类不能被继承:
final class Person {
protected String name;
}
// compile error: 不允许继承自Person
Student extends Person {
}
对于一个类的实例字段,同样可以用final
修饰。用final
修饰的字段在初始化后不能被修改。例如:
class Person {
public final String name = "Unamed";
}
对final
字段重新赋值会报错:
Person p = new Person();
p.name = "New Name"; // compile error!
可以在构造方法中初始化final字段:
class Person {
public final String name;
public Person(String name) {
this.name = name;
}
}
这种方法更为常用,因为可以保证实例一旦创建,其final
字段就不可修改。