面向对象的三个基本特征是:封装、继承、多态。
Class & Instance
- class是对象模板,instance是对象实例
- 定义class(包含多个字段,
field
)
1 | class Person { |
- 创建instance(指向
instance
的变量都是引用变量)
1 | Person ming = new Person(); |
- 访问实例变量用
变量.字段
- 一个Java源文件可以包含多个类的定义,但只能定义一个public类,且public类名必须与文件名一致。如果要定义多个public类,必须拆到多个Java源文件中
方法
- 动机:为了避免外部代码直接去访问
field
,我们可以用private
修饰field
,拒绝外部访问,然后使用method
来让外部代码可以间接修改field
- 在方法内部,我们有机会检查参数对不对;而外部代码没有任何机会把字段设置成不合理的值
- 一个类通过定义方法,可以给外部代码暴露一些操作的接口,同时保证内部逻辑一致性
- 调用方法的语法是
实例变量.方法名(参数);
- 定义方法
1 | // 修饰符 方法返回类型 方法名(方法参数列表) { |
- private方法:不允许外部调用,只有类的内部可以调用
- this变量:在方法内部,可以使用一个隐含的变量
this
,它始终指向当前实例。通过this.field
就可以访问当前实例的字段- 如果没有命名冲突,可以省略
this
;如果局部变量和字段重名,就必须加上this
- 如果没有命名冲突,可以省略
- 方法参数:0或任意个,用于接收传递给方法的变量值
- 可变参数:
类型…
,相当于数组类型- 可变参数可以保证无法传入
null
- 可变参数可以保证无法传入
- 参数绑定
- 基本类型参数的传递,是调用方值的复制。双方各自的后续修改,互不影响
- 引用类型参数的传递,调用方的变量和接收方的参数变量指向的是同一个对象。双方任意一方对这个对象的修改,都会影响对方(因为指向同一个对象)
构造方法
1 | public class Main { |
- 实例在创建时通过
new
操作符会调用其对应的构造方法,构造方法用于初始化实例- 创建实例实际上是通过构造方法来初始化实例的
- 构造方法的名称就是类名
- 构造方法的参数没有限制,在方法内部,也可以编写任意语句。但是,和普通方法相比,构造方法没有返回值(也没有
void
),调用构造方法,必须用new
- 没有定义构造方法时,编译器会自动创建一个默认的无参数构造方法
- 没有在构造方法中初始化字段时,引用类型的字段默认是
null
,数值类型的字段用默认值,int
类型默认值是0
,布尔类型默认值是false
多构造方法
- 可以定义多个构造方法,编译器根据参数自动判断
- 可以在一个构造方法内部调用另一个构造方法,便于代码复用
重载
1 | // String类提供了多个重载方法indexof() |
- 含义:方法重载是指多个方法的方法名相同,但各自的参数不同
- 作用:功能类似的方法使用同一名字,更容易记住,调用起来更简单
- 方法重载的返回值类型通常是相同的
继承
1 | class Person { |
- 作用:复用代码
- 使用
extends
关键字来实现继承 - 继承树:没有明确写
extends
的类,编译器会自动加上extends Object
- 任何类,除了
Object
,都会继承自某个类 - 一个类有且仅有一个父类。只有
Object
特殊,它没有父类
- 任何类,除了
- 子类无法访问父类的
private
,可以访问父类的protected
- 如果父类没有默认的构造方法,子类就必须显式调用
super()
并给出参数以便让编译器定位到父类的一个合适的构造方法- 子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的
阻止继承
1 | public sealed class Shape permits Rect, Circle, Triangle { |
- 从Java15开始,使用
sealed
修饰class,并通过permits
明确写出能够从该class继承的子类名称sealed
目前是预览状态,要启用它,必须使用参数--enable-preview
和--source 15
向上转型 Upcasting
1 | Person p = new Student(); |
- 把一个子类型安全地变为更加抽象的父类型
- 例如,继承树是
Student > Person > Object
,那么可以把Student
类型转型为Person
,或者更高层次的Object
- 例如,继承树是
向下转型 Downcasting
1 | Person p = new Student(); |
- 很可能会失败。为了避免出错,使用
instanceof
先判断一个实例究竟是不是某种类型 - 从Java 14开始,判断
instanceof
后,可以直接转型为指定变量,直接使用,避免再次强制转型
继承 & 组合
- 继承是is关系,组合是has关系
多态
1 | class Person { |
- 含义:针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法
- 作用:允许添加更多类型的子类实现功能扩展,却不需要修改基于父类的代码
覆写 Override
- 子类可以覆写父类的方法,覆写在子类中改变了父类方法的行为
- 加上
@Override
可以让编译器帮助检查是否进行了正确的覆写
- 加上
- 在子类的覆写方法中,如果要调用父类的被覆写的方法,可以通过
super
来调用 final
final
修饰的方法可以阻止被覆写final
修饰的class可以阻止被继承final
修饰的field必须在创建对象时初始化,随后不可修改
抽象类
1 | abstract class Person { |
- 动机:如果父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它,那么可以把父类的方法声明为抽象方法,本身没有实现任何方法语句
- 含义:如果一个
class
定义了方法,但没有具体执行代码,这个方法就是抽象方法,抽象方法用abstract
修饰;因为无法执行抽象方法,所以这个类也必须申明为抽象类(abstract class)- 抽象类无法实例化
- 作用:抽象类可以强迫子类实现其定义的抽象方法,否则编译会报错。相当于定义了“规范”
面向抽象编程
- 上层代码只定义规范
- 不需要子类就可以实现业务逻辑(正常编译)
- 具体的业务逻辑由不同的子类实现,调用者并不关心
接口
1 | interface Person { |
- 动机:如果一个抽象类没有字段,所有方法全部都是抽象方法,就可以把该抽象类改写为接口
- 接口是比抽象类还要抽象的纯抽象接口,因为它连字段都不能有
- 使用
interface
声明一个接口 - 当一个具体的
class
去实现一个interface
时,需要使用implements
关键字;一个类可以实现多个interface
- 接口也是数据类型,适用于向上转型和向下转型
接口继承
1 | interface Hello { |
- 使用
extends
- 继承关系
- 公共逻辑适合放在
abstract class
中,具体逻辑放到各个子类,而接口层次代表抽象程度 - 实例化的对象永远只能是某个具体的子类,但总是通过接口去引用它,因为接口比抽象类更抽象
- 公共逻辑适合放在
default方法
- 接口可以定义
default
方法 - 动机:当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是
default
方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法 default
方法和抽象类的普通方法的区别:因为interface
没有字段,所以default
方法无法访问字段,而抽象类的普通方法可以访问实例字段
静态字段 & 静态方法
静态字段
- 用
static
修饰的字段称为静态字段 - 实例字段&静态字段
- 实例字段在每个实例中都有自己的一个独立“空间”,各个实例的同名字段互不影响
- 静态字段只有一个共享“空间”,所有实例都会共享该字段
- 不推荐用
实例变量.静态字段
去访问静态字段,推荐用类名来访问静态字段- 可以把静态字段理解为描述
class
本身的字段(非实例字段)
- 可以把静态字段理解为描述
静态方法
- 用
static
修饰的方法称为静态方法 - 实例方法&静态方法
- 调用实例方法必须通过一个实例变量
- 调用静态方法则不需要实例变量,通过类名就可以调用。静态方法类似其它编程语言的函数
- 调用静态方法不需要实例,无法访问
this
,但可以访问静态字段和其他静态方法 - 静态方法常用于工具类(如
Arrays.sort()
,Math.random()
)和辅助方法(如Java程序的入口main()
就是静态方法)
接口的静态字段
interface
可以有静态字段,并且静态字段必须为final
类型- 因为
interface
的字段只能是public static final
类型,所以可以去掉这些修饰符,简写
包
1 | package ming; // 申明包名ming |
- 在定义
class
的时候,需要在第一行声明这个class
属于哪个包 - 一个类总是属于某个包(
package
),类名(比如Person
)只是一个简写,真正的完整类名是包名.类名
- 只要包名不同,类就不同
- 包可以是多层结构,用
.
隔开 - 包没有继承关系
- 没有定义包名的
class
使用的是默认包,非常容易引起名字冲突,不推荐不写包名的做法
包作用域
1 | package hello; |
- 位于同一个包的类,可以访问包作用域的字段和方法
- 不用
public
、protected
、private
修饰的字段和方法就是包作用域
import
1 | // Person.java |
- JDK的核心类使用
java.lang
包,编译器会自动导入 - JDK的其它常用类定义在
java.util.*
,java.math.*
,java.text.*
,……
作用域
public
- 定义为
public
的class
、interface
可以被其他任何类访问 - 定义为
public
的field
、method
可以被其他类访问,前提是首先有访问class
的权限 - 如果不确定是否需要
public
,就不声明为public
,即尽可能少地暴露对外的字段和方法 - 一个
.java
文件只能包含一个public
类,但可以包含多个非public
类。如果有public
类,文件名必须和public
类的名字相同
private
- 定义为
private
的field
、method
无法被其他类访问 private
访问权限被限定在class
的内部,而且与方法声明顺序无关。推荐把private
方法放到后面- 嵌套类(
nested class
)拥有访问private
的权限
protected
- 作用于继承关系。定义为
protected
的字段和方法可以被子类访问,以及子类的子类
pakage
- 包作用域是指一个类允许访问同一个
package
的没有public
、private
修饰的class
,以及没有public
、protected
、private
修饰的字段和方法 - 把方法定义为
package
权限有助于测试
局部变量
- 在方法内部定义的变量称为局部变量
- 局部变量作用域从变量声明处开始到对应的块结束
- 方法参数也是局部变量
- 应该尽可能把局部变量的作用域缩小,尽可能延后声明局部变量
final
- 可以阻止被继承、阻止被子类覆写、阻止被重新赋值
内部类 Nested Class
Inner Class
1 | public class Main { |
- Inner Class的实例不能单独存在,必须依附于一个Outer Class的实例
- 要实例化一个
Inner
,必须首先创建一个Outer
的实例,然后,调用Outer
实例的new
来创建Inner
实例
- 要实例化一个
- Inner Class可以修改Outer Class的
private
字段 Outer
类被编译为Outer.class
,而Inner
类被编译为Outer$Inner.class
Anonymous Class
1 | Runnable r = new Runnable() { |
- 另一种定义Inner Class的方法。它不需要在Outer Class中明确地定义这个Class
- 动机:因为在这里我们不关心类名,比直接定义Inner Class可以少写很多代码
- 匿名类也可以访问Outer Class的
private
字段和方法 - 匿名类被编译为
Outer$1.class
、Outer$2.class
、Outer$3.class
… - 除了接口外,匿名类也可以继承自普通类
- Inner Class和Anonymous Class本质上是相同的,都必须依附于Outer Class的实例,即隐含地持有
Outer.this
实例,并拥有Outer Class的private
访问权限
Static Nested Class
- 不再依附于
Outer
的实例,而是一个完全独立的类,因此无法引用Outer.this
,但可以访问Outer
的private
静态字段和静态方法 - Static Nested Class是独立类,但拥有Outer Class的
private
访问权限
classpath和jar
- JVM通过环境变量
classpath
决定搜索class
的路径和顺序 - 不推荐设置系统环境变量
classpath
,始终建议通过-cp
命令传入 - jar包相当于目录,可以包含很多
.class
文件,方便下载和使用
class版本
- 高版本的JDK可编译输出低版本兼容的class文件,反之可能报错
- 运行时使用哪个JDK版本,编译时就尽量使用同一版本编译源码
模块
.class
文件是JVM看到的最小可执行文件,而一个大型程序需要编写很多Class,并生成一堆.class
文件。jar
文件就是class
文件的容器,为了方便管理。它并不关心class
之间的依赖- 从Java 9开始引入了模块(Module),自带“依赖关系”的class容器就是模块,模块以
.jmod
扩展名标识 - 只有
java.base
模块不依赖任何模块,是“根模块” - 把一堆class封装为jar仅仅是一个打包的过程,而把一堆class封装为模块则不但需要打包,还需要写入依赖关系,并且可以包含二进制代码(通常是JNI扩展)、支持多版本
- 模块的作用:声明并管理依赖关系;进一步隔离代码的访问权限
附录
教程
评论