本文主要内容为面向对象基础内容,包括方法、构造方法、重载、继承、多态等。
方法
把field
从public
改成private
,外部代码不能访问这些field
,所以我们需要使用方法(method
)来让外部代码可以间接修改field
:
1 | public class Main { |
虽然外部代码不能直接修改private
字段,但是,外部代码可以调用方法setName()
和setAge()
来间接修改private
字段。在方法内部,我们就可以检查参数。比如,setAge()
就会检查传入的参数,参数超出了范围,直接报错。这样,外部代码就没有任何机会把age
设置成不合理的值。
一个类通过定义方法,就可以给外部代码暴露一些操作的接口,同时,内部自己保证逻辑一致性。
定义方法
定义方法的语法是:
1 | 修饰符 方法返回类型 方法名(方法参数列表) { |
this
在方法内部,可以使用一个隐含的变量this
,它始终指向当前实例。因此,通过this.field
就可以访问当前实例的字段。
如果没有命名冲突,可以省略this
。例如:
1 | class Person { |
但是,如果有局部变量和字段重名,那么局部变量优先级更高,就必须加上this
说明指向当前实例:
1 | class Person { |
可变参数
可变参数用类型...
定义,可变参数相当于数组类型:
1 | class Group { |
setNames()
就定义了一个可变参数。调用时,可以这么写:
1 | Group g = new Group(); |
参数绑定
调用方把参数传递给实例方法时,调用时传递的值会按参数位置一一绑定。
基本参数类型绑定
1 | public static void main(String[] args) { |
结论:基本类型参数的传递,是调用方值(上面代码传递的为15)的复制。双方各自的后续修改,互不影响。
引用参数类型绑定
1 | public class Main { |
结论:引用类型参数的传递,调用方的变量和接收方的参数变量,指向的是同一个对象。双方任意一方对这个对象的修改,都会影响对方。
构造方法
在创建对象实例时就把内部字段全部初始化为合适的,我们就需要构造方法。
创建实例的时候,实际上是通过构造方法来初始化实例的。
1 | public class Main { |
构造方法的名称就是类名。构造方法的参数没有限制,在方法内部,也可以编写任意语句。但是,和普通方法相比,构造方法没有返回值(也没有void
),调用构造方法,必须用new
操作符。
默认构造方法
如果一个类没有定义构造方法,编译器会自动为我们生成一个默认构造方法,它没有参数,也没有执行语句,类似这样:
1 | class Person { |
如果我们自定义了一个构造方法,那么,编译器就不再自动创建默认构造方法。
如果既要能使用带参数的构造方法,又想保留不带参数的构造方法,那么只能把两个构造方法都定义出来。
没有在构造方法中初始化字段时,引用类型的字段默认是null
,数值类型的字段用默认值,int
类型默认值是0
,布尔类型默认值是false
。
在Java中,创建对象实例的时候,按照如下顺序进行初始化:
- 先初始化字段,例如,
int age = 10;
表示字段初始化为10
,double salary;
表示字段默认初始化为0
,String name;
表示引用类型字段默认初始化为null
; - 执行构造方法的代码进行初始化。
多构造方法
可以定义多个构造方法,在通过new
操作符调用的时候,编译器通过构造方法的参数数量、位置和类型自动区分:
1 | class Person { |
一个构造方法可以调用其他构造方法,这样做的目的是便于代码复用。调用其他构造方法的语法是this(…)
:
1 | class Person { |
小结
实例在创建时通过
new
操作符会调用其对应的构造方法,构造方法用于初始化实例;没有定义构造方法时,编译器会自动创建一个默认的无参数构造方法;
可以定义多个构造方法,编译器根据参数自动判断;
可以在一个构造方法内部调用另一个构造方法,便于代码复用。
方法重载
方法名相同,但各自的参数不同,称为方法重载(Overload
)
注意:方法重载的返回值类型通常都是相同的。
方法重载的目的是,功能类似的方法使用同一名字,更容易记住,因此,调用起来更简单。
重载方法indexOf()
String
类提供了多个重载方法indexOf()
,可以查找子串:
int indexOf(int ch)
:根据字符的Unicode码查找;int indexOf(String str)
:根据字符串查找;int indexOf(int ch, int fromIndex)
:根据字符查找,但指定起始位置;int indexOf(String str, int fromIndex)
根据字符串查找,但指定起始位置。
小结
方法重载是指多个方法的方法名相同,但各自的参数不同;
重载方法应该完成类似的功能,参考
String
的indexOf()
;重载方法返回值类型应该相同。
继承
定义
Java使用extends
关键字来实现继承:
1 | class Person { |
在OOP的术语中,我们把Person
称为超类(super class),父类(parent class),基类(base class),把Student
称为子类(subclass),扩展类(extended class)。
Java只允许一个class继承自一个类,因此,一个类有且仅有一个父类。只有Object
特殊,它没有父类。
protected
为了让子类可以访问父类的字段,我们需要把private
改为protected
。用protected
修饰的字段可以被子类访问。
protected
关键字可以把字段和方法的访问权限控制在继承树内部,一个protected
字段和方法可以被其子类,以及子类的子类所访问。
super
super
关键字表示父类(超类)。子类引用父类的字段时,可以用super.fieldName
。
1 | public class Main { |
编译错误,原因是在Student
的构造方法中,无法调用父类Person
的构造方法。
在Java中,任何class
的构造方法,第一行语句必须是调用父类的构造方法。如果没有显式调用父类的构造方法,编译器会帮我们自动加一句super();
Student
类的构造方法实际上是这样:
1 | class Student extends Person { |
但是,Person
类并没有无参数的构造方法,因此,编译失败。
解决方法是调用Person
类存在的某个构造方法。例如:
1 | class Student extends Person { |
向上转型
把一个子类类型安全地变为父类类型的赋值,被称为向上转型(upcasting)。
继承树是Student > Person > Object
,所以,可以把Student
类型转型为Person
,或者更高层次的Object
。
向上转型实际上是把一个子类型安全地变为更加抽象的父类型:
1 | Student s = new Student(); |
向下转型
把一个父类类型强制转型为子类类型,就是向下转型(downcasting)
不能把父类变为子类,因为子类功能比父类多,向下转型很可能会失败。失败的时候,Java虚拟机会报ClassCastException
。
利用instanceof
,在向下转型前可以先判断:
1 | Person p = new Student(); |
区分继承和组合
继承是is关系,组合是has关系。
Student
是Person
的一种,它们是is关系,而Student
并不是Book
,Student
和Book
的关系是has关系。
具有has关系不应该使用继承,而是使用组合,即Student
可以持有一个Book
实例:
1 | class Student extends Person { |
小结
- 继承是面向对象编程的一种强大的代码复用方式;
- Java只允许单继承,所有类最终的根类是
Object
; protected
允许子类访问父类的字段和方法;- 子类的构造方法可以通过
super()
调用父类的构造方法; - 可以安全地向上转型为更抽象的类型;
- 可以强制向下转型,最好借助
instanceof
判断; - 子类和父类的关系是is,has关系不能用继承。
多态
覆写
在继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为覆写(Override)。
在Person
类中,我们定义了run()
方法:
1 | class Person { |
在子类Student
中,覆写这个run()
方法:
1 | class Student extends Person { |
Java的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。
多态Polymorphic
针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法。对某个类型调用某个方法,执行的实际方法可能是某个子类的覆写方法。
多态具有一个非常强大的功能,就是允许添加更多类型的子类实现功能扩展,却不需要修改基于父类的代码。
1 | public class Main { |
覆写Object方法
Object
定义了几个重要的方法:
toString()
:把instance输出为String
;equals()
:判断两个instance是否逻辑相等;hashCode()
:计算一个instance的哈希值。
在必要的情况下,我们可以覆写Object
的这几个方法。
1 | class Person { |
调用父类的被覆写的方法
通过super
来调用
1 | class Person { |
final
因为继承可以使得子类覆写父类的方法,则
如果一个父类不允许子类对父类的某个方法进行覆写,可以把该方法标记为
final
,用final
修饰的方法不能被Override
:如果一个类不希望被其他类继承,那么可以把这个类本身标记为
final
,用final
修饰的类不能被继承一个类的实例字段,用
final
修饰的字段在初始化后不能被修改。
在构造方法中初始化final字段:
1 | class Person { |
常用,因为可以保证实例一旦创建,其final
字段就不可修改。
小结
- 子类可以覆写父类的方法(Override),覆写在子类中改变了父类方法的行为;
- Java的方法调用总是作用于运行期对象的实际类型,这种行为称为多态;
final
修饰符有多种作用:final
修饰的方法可以阻止被覆写;final
修饰的class可以阻止被继承;final
修饰的field必须在创建对象时初始化,随后不可修改。
抽象类
如果父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它,那么,可以把父类的方法声明为抽象方法:
1 | class Person { |
把一个方法声明为abstract
,表示它是一个抽象方法,本身没有实现任何方法语句。因为这个抽象方法本身是无法执行的,所以,Person
类也无法被实例化。编译器会告诉我们,无法编译Person
类,因为它包含抽象方法。
必须把Person
类本身也声明为abstract
,才能正确编译它:
1 | abstract class Person { |
抽象类
如果一个class
定义了方法,但没有具体执行代码,这个方法就是抽象方法,抽象方法用abstract
修饰。
因为无法执行抽象方法,因此这个类也必须申明为抽象类(abstract class)。
因为抽象类本身被设计成只能用于被继承,因此,抽象类可以强迫子类实现其定义的抽象方法,否则编译会报错。
例如,Person
类定义了抽象方法run()
,那么,在实现子类Student
的时候,就必须覆写run()
方法:
1 | public class Main { |
面向抽象编程
引用高层类型,避免引用实际子类型的方式,称之为面向抽象编程。
面向抽象编程的本质就是:
- 上层代码只定义规范(例如:
abstract class Person
); - 不需要子类就可以实现业务逻辑(正常编译);
- 具体的业务逻辑由不同的子类实现,调用者并不关心。
小结
- 通过
abstract
定义的方法是抽象方法,抽象方法只有定义,没有实现。抽象方法定义了子类必须实现的接口规范; - 定义了抽象方法的class必须被定义为抽象类,从抽象类继承的子类必须实现抽象方法;
- 如果不实现抽象方法,则该子类仍是一个抽象类;
- 面向抽象编程使得调用者只关心抽象方法的定义,不关心子类的具体实现。
接口
在抽象类中,抽象方法本质上是定义接口规范:即规定高层类的接口,从而保证所有子类都有相同的接口实现
如果一个抽象类没有字段,所有方法全部都是抽象方法:
1 | abstract class Person { |
应该把该抽象类改写为接口:interface
。
使用interface
可以声明一个接口:
1 | interface Person { |
因为接口定义的所有方法默认都是public abstract
的,所以这两个修饰符不需要写出来。
当一个具体的class
去实现一个interface
时,需要使用implements
关键字。
1 | class Student implements Person { |
在Java中,一个类只能继承自另一个类,不能从多个类继承。但是,一个类可以实现多个interface
,例如:
1 | class Student implements Person, Hello { // 实现了两个interface |
抽象类和接口的对比
abstract class | interface | |
---|---|---|
继承 | 只能extends一个class | 可以implements多个interface |
字段 | 可以定义实例字段 | 不能定义实例字段 |
抽象方法 | 可以定义抽象方法 | 可以定义抽象方法 |
非抽象方法 | 可以定义非抽象方法 | 可以定义default方法 |
接口继承
一个interface
可以继承自另一个interface
。interface
继承自interface
使用extends
,它相当于扩展了接口的方法。例如:
1 | interface Hello { |
此时,Person
接口继承自Hello
接口,因此,Person
接口现在实际上有3个抽象方法签名,其中一个来自继承的Hello
接口。
default方法
在接口中,可以定义default
方法。
default
方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是default
方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。
静态字段和方法
实例字段:class
中定义的字段
静态字段static field
:用static
修饰的字段
实例字段在每个实例中都有自己的一个独立“空间”,但是静态字段只有一个共享“空间”,所有实例都会共享该字段。
1 | public class Main { |
输出:
1 | 88 |
因为在Java程序中,实例对象并没有静态字段,不推荐用实例变量.静态字段
去访问静态字段。
类名.静态字段
来访问静态对象。
上述访问静态字段的代码应该为:
1 | Person.number = 99; |
静态方法
静态方法:用static
修饰的方法
调用实例方法必须通过一个实例变量,调用静态方法则不需要实例变量,通过类名就可以调用。
1 | public class Main { |
因为静态方法属于class
而不属于实例,因此,静态方法内部,无法访问this
变量,也无法访问实例字段,它只能访问静态字段。
Person.setNumber(99); 不需实例化,通过类名调用
接口的静态字段
interface
是可以有静态字段的,并且静态字段必须为final
类型。
interface
的字段只能是public static final
类型。
1 | public interface Person { |
包
Java定义了一种名字空间,称之为包:package
。一个类总是属于某个包,类名(比如Person
)只是一个简写,真正的完整类名是包名.类名
。
1 | package pk; // 申明包名pk |
包没有父子关系,java.util和java.util.zip是不同的包,两者没有任何继承关系。
小结
Java内建的package
机制是为了避免class
命名冲突;
JDK的核心类使用java.lang
包,编译器会自动导入;
JDK的其它常用类定义在java.util.*
,java.math.*
,java.text.*
,……;
包名推荐使用倒置的域名,例如org.apache
。
作用域
public
定义为public
的class
、interface
可以被其他任何类访问
private
private
访问权限被限定在class
的内部,而且与方法声明顺序无关。推荐把private
方法放到后面,因为public
方法定义了类对外提供的功能。
protected
protected
作用于继承关系。定义为protected
的字段和方法可以被子类访问,以及子类的子类。
final
Java还提供了一个final
修饰符。**final
与访问权限不冲突**,它有很多作用。
用final
修饰class
可以阻止被继承,用final
修饰method
可以阻止被子类覆写,用final
修饰field
可以阻止被重新赋值,用final
修饰局部变量可以阻止被重新赋值。
小结
一个
.java
文件只能包含一个public
类,但可以包含多个非public
类。如果有public
类,文件名必须和public
类的名字相同。final
修饰符不是访问权限,它可以修饰class
、field
和method
。