。。。
目录
一、类与对象
1.1、类与对象的概念
1.1.1、概念
class:是一种对象模版,它定义了如何创建实例,因此,class本身就是一种数据类型
instance:是对象实例,instance是根据class创建的实例,可以创建多个instance,每个instance类型相同,但各自属性可能不相同
1.1.2、面向对象的特征
面向对象的基本特征是封装、继承、多态
封装:具有两层含义: (1)把对象的全部属性和基于数据的方法封装在一起,形成为一个整体对象。(2)隐蔽信息,即尽可能的隐蔽对象的内部细节,对外形成一个屏障,只保留有限的接口使之与外界发生联系。这样可以极大的保证内部数据的安全性,可以避免外部环境对内部数据的侵扰和破坏。
继承:是指一个类拥有另一个类的数据和操作。被继承的类称为父类,继承了父类的数据和操作的类称为子类。通过类的继承,可以实现程序代码的重复使用,使得程序结构清晰,降低编程和维护的工作量。
Java只支持单重继承,不支持多重继承,即一个子类只能有一个父类,但一个父类可以有多个子类,这样使得Java的继承层次形成了树状结构。单重继承使得程序结构变得简单,对于一些复杂的现实问题,当用单重继承不能解决时,Java可以通过实现接口的方式来弥补单重继承的不足,以达到解决复杂问题的目的。
多态:是指程序的多种表现形式,是面向对象程序设计代码重用的一个重要机制。Java中的多态分为方法多态和变量多态,方法多态分为方法重载和方法覆盖。
1.2、类的定义与使用
1.2.1、类的定义—创建类
创建一个类,例如,给这个类命名为Person
,就是定义一个class
:
class Person {
public String name;
public int age;
}
一个class
可以包含多个字段(field
),字段用来描述一个类的特征。
上面的Person
类,我们定义了两个字段,一个是String
类型的字段,命名为name
,一个是int
类型的字段,命名为age
。
因此,通过class
,把一组数据汇集到一个对象上,实现了数据封装。
public
是用来修饰字段的,它表示这个字段可以被外部访问。
1.1.2、类的使用—创建对象
对象是根据类创建的。在Java中,使用关键字new来创建一个新的对象。创建对象需要以下三步:
- 声明:声明一个对象,包括对象名称和对象类型。
- 实例化:使用关键字new来创建一个对象。
- 初始化:使用new创建对象时,会调用构造方法初始化对象。
定义了class,只是定义了对象模版,而要根据对象模版创建出真正的对象实例,必须用new操作符。
new操作符,可以创建一个实例,然后,我们需要定义一个引用类型的变量来指向这个实例:
Person ming = new Person();
上述代码创建了一个Person类型的实例,并通过变量ming
指向它。
注意区分Person ming
是定义Person
类型的变量ming
,而new Person()
是创建Person
实例。
访问实例变量和方法
有了指向这个实例的变量,我们就可以通过这个变量来操作实例。访问实例变量可以用变量.字段
,例如:
ming.name = "Xiao Ming"; // 对字段name赋值
ming.age = 12; // 对字段age赋值
System.out.println(ming.name); // 访问字段name
Person hong = new Person();
hong.name = "Xiao Hong";
hong.age = 15;
1.3、源文件声明规则
当在一个源文件中定义多个类,并且还有import语句和package语句时,要特别注意这些规则。
- 一个源文件中只能有一个public类
- 一个源文件可以有多个非public类
- 源文件的名称应该和public类的类名保持一致。例如:源文件中public类的类名是Employee,那么源文件应该命名为Employee.java。
- 如果一个类定义在某个包中,那么package语句应该在源文件的首行。
- 如果源文件包含import语句,那么应该放在package语句和类定义之间。如果没有package语句,那么import语句应该在源文件中最前面。
- import语句和package语句对源文件中定义的所有类都有效。在同一源文件中,不能给不同的类不同的包声明。
类有若干种访问级别,并且类也分不同的类型:抽象类和final类等。这些将在访问控制章节介绍。
除了上面提到的几种类型,Java还有一些特殊的类,如:内部类、匿名类。
二、方法
一个class
可以包含多个field
,但是,直接把field
用public
暴露给外部可能会破坏封装性。为了避免外部代码直接去访问field
,我们可以用private
修饰field
,拒绝外部访问。
把field
从public
改成private
,外部代码不能访问这些field
,需要使用方法(method
)来让外部代码可以间接修改field,
外部代码可以调用方法来间接修改private
字段,在方法内部,我们就有机会检查参数对不对。
-
方法可以让外部代码安全地访问实例字段;
-
方法是一组执行语句,并且可以执行任意逻辑;
-
方法内部遇到return时返回,void表示不返回任何值(注意和返回null不同);
-
外部代码通过public方法操作实例,内部代码可以调用private方法;
-
理解方法的参数绑定。
2.1、方法的定义与使用
2.1.1、定义
从上面的代码可以看出,定义方法的语法是:
修饰符 方法返回类型 方法名(方法参数列表) {
若干方法语句;
return 方法返回值;
}
方法返回值通过return
语句实现,如果没有返回值,返回类型设置为void
,可以省略return
。
class Person {
private String name;
public int age;
public String getName() {
return this.name;
}
public void setName(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("invalid name");
}
this.name = name.strip(); // 去掉首尾空格
}
2.2.2、private方法
有public
方法,自然就有private
方法。和private
字段一样,private
方法不允许外部调用,那我们定义private
方法有什么用?
定义private
方法的理由,是内部方法是可以调用private
方法的。例如:
class Person {
private String name;
private int birth;
public void setBirth(int birth) {
this.birth = birth;
}
public int getAge() {
return calcAge(2019); // ===调用private方法
}
// =====private方法:
private int calcAge(int currentYear) {
return currentYear - this.birth;
}
}
观察上述代码,calcAge()
是一个private
方法,外部代码无法调用,但是,内部方法getAge()
可以调用它。
此外,我们还注意到,这个Person
类只定义了birth
字段,没有定义age
字段,获取age
时,通过方法getAge()
返回的是一个实时计算的值,并非存储在某个字段的值。这说明方法可以封装一个类的对外接口,调用方不需要知道也不关心Person
实例在内部到底有没有age
字段。
2.2.3、this变量
在方法内部,可以使用一个隐含的变量this
,它始终指向当前实例。因此,通过this.field
就可以访问当前实例的字段。
如果没有命名冲突,可以省略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了
}
}
2.2.4、方法的调用
public class Main {
public static void main(String[] args) {
Person ming = new Person();
ming.setBirth(2008);
System.out.println(ming.getAge());
}
}
2.2、方法的参数
2.2.1、方法参数
方法可以包含0个或任意个参数。方法参数用于接收传递给方法的变量值。
调用方法时,必须严格按照参数的定义一一传递。例如:
class Person {
...
public void setNameAndAge(String name, int age) {
...
}
}
调用这个setNameAndAge()
方法时,必须有两个参数,且第一个参数必须为String
,第二个参数必须为int
:
Person ming = new Person();
ming.setNameAndAge("Xiao Ming"); // 编译错误:参数个数不对
ming.setNameAndAge(12, "Xiao Ming"); // 编译错误:参数类型不对
2.2.2、可变参数
可变参数用类型...
定义,可变参数相当于数组类型:
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[]
另一个问题是,调用方可以传入null
:
Group g = new Group();
g.setNames(null);
而可变参数可以保证无法传入null
,因为传入0个参数时,接收到的实际值是一个空数组而不是null
。
2.2.3、参数绑定
调用方 把参数传递给 实例方法时,调用时传递的值会按参数位置一一绑定。
基本类型参数:八大基本类型 引用类型参数:Sring等
基本类型参数的传递,是调用方值的复制。双方各自的后续修改,互不影响。
引用类型参数的传递,调用方的变量,和接收方的参数变量,指向的是同一个对象。双方任意一方对这个对象的修改,都会影响对方(因为指向同一个对象嘛)。
2.3、构造方法
构造方法:创建对象实例时,就把内部字段,全部初始化为合适的值
构造方法的名称,就是类名
构造方法的参数,没有限制,在方法内部,也可以编写任意语句。
但是,和普通方法相比,构造方法没有返回值(也没有void
),调用构造方法,必须用new
操作符
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;
}
}
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());
}
}
2.3.1、默认构造方法
是不是任何class
都有构造方法?是的。那前面我们并没有为Person
类编写构造方法,为什么可以调用new Person()
?
如果一个类没有定义构造方法,编译器会自动为我们生成一个默认构造方法,它没有参数,也没有执行语句,类似这样:
class Person {
public Person() {
}
}
注意:若,自定义了一个构造方法,则编译器就不再自动创建默认构造方法
没有在构造方法中初始化字段时,
引用类型的字段默认是null
,数值类型的字段用默认值,int
类型默认值是0
,布尔类型默认值是false
:
class Person {
private String name; // 默认初始化为null
private int age; // 默认初始化为0
public Person() {
}
}
也可以对字段直接进行初始化:
class Person {
private String name = "Unamed";
private int age = 10;
}
那么问题来了:既对字段进行初始化,又在构造方法中对字段进行初始化:
class Person {
private String name = "Unamed";
private int age = 10;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
当我们创建对象的时候,new Person("Xiao Ming", 12)
得到的对象实例,字段的初始值是啥?
在Java中,创建对象实例的时候,按照如下顺序进行初始化:
-
先初始化字段;
-
执行构造方法的代码进行初始化。
因此,构造方法的代码由于后运行,所以,new Person("Xiao Ming", 12)
的字段值最终由构造方法的代码确定。
2.3.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)
。
如果调用new Person("Xiao Ming");
,会自动匹配到构造方法public Person(String)
。
如果调用new Person();
,会自动匹配到构造方法public Person()
。
一个构造方法可以调用其他构造方法,这样做的目的是便于代码复用。调用其他构造方法的语法是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)
}
}
2.4、方法重载
在一个类中,我们可以定义多个方法。
如果有一系列方法,它们的功能都是类似的,只有参数有所不同,
那么,可以把这一组方法名做成同名方法。
这种方法名相同,但各自的参数不同,称为方法重载(Overload
)。
例如,在Hello
类中,定义多个hello()
方法:
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 + "!");
}
}
}
注意:方法重载的返回值类型通常都是相同的。
方法重载的目的是,功能类似的方法使用同一名字,更容易记住,因此,调用起来更简单
三、继承
-
继承是面向对象编程的一种强大的代码复用方式;
-
Java只允许单继承,所有类最终的根类是
Object
; -
protected
允许子类访问父类的字段和方法; -
子类的构造方法可以通过
super()
调用父类的构造方法; -
可以安全地向上转型为更抽象的类型;
-
可以强制向下转型,最好借助
instanceof
判断; -
子类和父类的关系是is,has关系不能用继承。
继承是面向对象编程中非常强大的一种机制,它首先可以复用代码。
当我们让Student
从Person
继承时,Student
就获得了Person
的所有功能,我们只需要为Student
编写新增的功能。
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)。
注意到,我们在定义Person
的时候,没有写extends
。在Java中,没有明确写extends
的类,编译器会自动加上extends Object
。所以,任何类,除了Object
,都会继承自某个类。
Java只允许一个class继承自一个类,因此,一个类有且仅有一个父类。只有Object
特殊,它没有父类。
-
3.1 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! } }
- 继承有个特点,就是子类无法访问父类的
-
3.2 super
super
关键字表示父类(超类)。- 子类引用父类的字段时,可以用
super.fieldName
。 -
class Student extends Person { public String hello() { return "Hello, " + super.name; } }
这里使用
super.name
,或者this.name
,或者name
,效果都是一样的。编译器会自动定位到父类的name
字段。
在Java中,任何class
的构造方法,第一行语句必须是调用父类的构造方法。
如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句super();
,所以,Student
类的构造方法实际上是这样:
class Student extends Person {
protected int score;
public Student(String name, int age, int score) {
super(); // 自动调用父类的构造方法
this.score = score;
}
}
但是,Person
类并没有无参数的构造方法,因此,编译失败。
解决方法 :调用Person
类存在的某个构造方法。例如:
class Student extends Person {
protected int score;
public Student(String name, int age, int score) {
super(name, age); // 调用父类的构造方法Person(String, int)
this.score = score;
}
}
这样就可以正常编译了!
因此我们得出结论:
如果父类没有默认的构造方法,子类就必须显式调用super()
并给出参数以便让编译器定位到父类的一个合适的构造方法。
这里还顺带引出了另一个问题:
即 子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。
3.3 向上转型:把一个子类型安全地转为更加抽象的父类型
如果一个引用变量的类型是Student
,那么它可以指向一个Student
类型的实例:
Student s = new Student();
如果一个引用类型的变量是Person
,那么它可以指向一个Person
类型的实例:
Person p = new Person();
现在问题来了:如果Student
是从Person
继承下来的,
那么,一个引用类型为Person
的变量,能否指向Student
类型的实例?
Person p = new Student(); // ???
测试一下就可以发现,这种指向是允许的!
这是因为Student
继承自Person
,因此,它拥有Person
的全部功能。
Person
类型的变量,如果指向Student
类型的实例,对它进行操作,是没有问题的!
这种把一个子类类型安全地变为父类类型的赋值,被称为向上转型(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
。
3.3 向下转型:父类类型强制转为子类类型
和向上转型相反,如果把一个父类类型强制转型为子类类型,就是向下转型(downcasting)。例如:
Person p1 = new Student(); // Person类型变量p1 实际指向Student实例
Person p2 = new Person(); // Person类型变量p2 实际指向Person实例
Student s1 = (Student) p1; // ok
Student s2 = (Student) p2; // runtime error! ClassCastException!
如果测试上面的代码,可以发现:
在向下转型的时候,把p1
转型为Student
会成功,因为p1
确实指向Student
实例,
把p2
转型为Student
会失败,因为p2
的实际类型是Person
,不能把父类变为子类,因为子类功能比父类多,多的功能无法凭空变出来。
因此,向下转型很可能会失败。失败的时候,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; // 一定会成功
}
3.4 区分继承和组合
在使用继承时,我们要注意逻辑一致性。
考察下面的Book
类:
class Book {
protected String name;
public String getName() {...}
public void setName(String name) {...}
}
这个Book
类也有name
字段,那么,我们能不能让Student
继承自Book
呢?
class Student extends Book {
protected int score;
}
显然,从逻辑上讲,这是不合理的,Student
不应该从Book
继承,而应该从Person
继承。
究其原因,是因为Student
是Person
的一种,它们是is关系,而Student
并不是Book
。实际上Student
和Book
的关系是has关系。
具有has关系不应该使用继承,而是使用组合,即Student
可以持有一个Book
实例:
class Student extends Person {
protected Book book;//======
protected int score;
}
因此,继承是is关系,组合是has关系。
四、多态
4.1 覆写(Override)
在继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为覆写(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
。
注意:方法名相同,方法参数相同,但方法返回值不同,也是不同的方法。在Java程序中,出现这种情况,编译器会报错。
class Person {
public void run() { … }
}
class Student extends Person {
public void run(String s) { … } // 不是Override,因为参数不同:
public int run() { … } // 不是Override,因为返回值不同:
}
@Override
不是必需的,加上@Override
可以让编译器帮助检查是否进行了正确的覆写。希望进行覆写,但是不小心写错了方法签名,编译器会报错。
在上一节中,我们已经知道,引用变量的声明类型可能与其实际类型不符,例如:
Person p = new Student();
一个实际类型为Student
,引用类型为Person
的变量,调用其run()
方法,调用的是Person
还是Student
的run()
方法?
实际上调用的方法是Student
的run()
方法。因此可得出结论:
Java的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。
这个非常重要的特性在面向对象编程中称之为多态。它的英文拼写非常复杂:Polymorphic。
4.2、多态 Polymorphic
多态是指,针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法。例如:
Person p = new Student();
p.run(); // 无法确定运行时究竟调用哪个run()方法
有童鞋会问,从上面的代码一看就明白,肯定调用的是Student
的run()
方法啊。
但是,假设我们编写这样一个方法:
public void runTwice(Person p) {
p.run();
p.run();
}
它传入的参数类型是Person
,我们是无法知道传入的参数实际类型究竟是Person
,还是Student
,还是Person
的其他子类,因此,也无法确定调用的是不是Person
类定义的run()
方法。
所以,多态的特性就是,运行期才能动态决定调用的子类方法。对某个类型调用某个方法,执行的实际方法可能是某个子类的覆写方法。这种不确定性的方法调用,究竟有什么作用?
我们还是来举栗子。
假设我们定义一种收入,需要给它报税,那么先定义一个Income
类:
class Income {
protected double income;
public double getTax() {
return income * 0.1; // 税率10%
}
}
对于工资收入,可以减去一个基数,那么我们可以从Income
派生出SalaryIncome
,并覆写getTax()
:
class Salary extends Income {
@Override
public double getTax() {
if (income <= 5000) {
return 0;
}
return (income - 5000) * 0.2;
}
}
如果你享受国务院特殊津贴,那么按照规定,可以全部免税:
class StateCouncilSpecialAllowance extends Income {
@Override
public double getTax() {
return 0;
}
}
现在,我们要编写一个报税的财务软件,对于一个人的所有收入进行报税,可以这么写:
public double totalTax(Income... incomes) {
double total = 0;
for (Income income: incomes) {
total = total + income.getTax();
}
return total;
}
观察totalTax()
方法:利用多态,totalTax()
方法只需要和Income
打交道,它完全不需要知道Salary
和StateCouncilSpecialAllowance
的存在,就可以正确计算出总的税。如果我们要新增一种稿费收入,只需要从Income
派生,然后正确覆写getTax()
方法就可以。把新的类型传入totalTax()
,不需要修改任何代码。
可见,多态具有一个非常强大的功能,就是允许添加更多类型的子类实现功能扩展,却不需要修改基于父类的代码。
覆写Object方法
因为所有的class
最终都继承自Object
,而Object
定义了几个重要的方法:
toString()
:把instance输出为String
;equals()
:判断两个instance是否逻辑相等;hashCode()
:计算一个instance的哈希值。
在必要的情况下,我们可以覆写Object
的这几个方法。例如:
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;
}
// 计算hash:
@Override
public int hashCode() {
return this.name.hashCode();
}
}
调用super
在子类的覆写方法中,如果要调用父类的被覆写的方法,可以通过super
来调用。例如:
class Person {
protected String name;
public String hello() {
return "Hello, " + name;
}
}
Student extends Person {
@Override
public String hello() {
// 调用父类的hello()方法:
return super.hello() + "!";
}
}
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
字段就不可修改。