Java核心技术卷Ⅰ
.jpg)
Java核心技术卷Ⅰ
Alive~o.0对象与类
面向对象程序设计概述
创建实例:类构建对象的过程
实例字段:对象中的数据;实例字段的值的集合称为状态
方法:操作数据的过程
行为、状态、标识
类之间的关系
- 依赖
- 聚合
- 继承
使用预定义类
对象与对象变量
使用构造器(constructor)构造新实例,构造器的名字应该与类名相同。
一个对象变量并没有实际包含一个对象,而仅仅引用一个对象。
局部变量不会自动地初始化为null,而必须通过调用new或将它们设置为null进初始化。
更改器方法与访问器方法
只访问对象而不修改对象的方法有时称为访问器方法(accessor method)例如,LocalDate.getYear和GregorianCalendar.get。
GregorianCalendar.add方法是一个更改器方法(mutator method)。调用这个方法后,someDay对象的状态会改变。
自定义类
Employee类
文件名必须与public类的名字相匹配。在一个源文件中,只能有一个公有类,但可以有任意数目的非公有类。
类通常包含类类型的实例字段。
1 | private String name; |
构造器
构造器与类同名,总是伴随着new操作符的执行被调用,而不能对一个已经存在的对象调用构造器来达到重新设置实例域的目的。
1 | public Employee(String n, double s, int year, int month, int day) |
- 构造器与类同名
- 每个类可以有一个以上的构造器
- 构造器可以有0个、1个或多个参数
- 构造器没有返回值
- 构造器总是伴随着new操作一起调用
- 所有的Java对象都是在堆中构造的
- 不要在构造器中定义与实例域重名的局部变量。这些变量只能在构造器内部访问。这些变量屏蔽了同名的实例域。
var关键字声明局部变量:
1 | var harry = new Employee("Carl Cracker", 75000, 1987, 12, 15); |
只能用于方法中的局部变量,参数和字段的类型必须声明。
使用null引用:
null值应用一个方法,产生NullPointException异常
1 | LocalDate rightNow = null; |
隐式参数与显式参数
第一个参数称为隐式(implicit)参数,是出现在方法名前的Employee类对象。第二个参数位于方法名后面括号中的数值,这是一个显式(explicit)参数。在每一个方法中,关键字this表示隐式参数。
1 | public void raiseSalary(double byPercent){ |
封装的优点
需要获得或设置实例域的值。因此,应该提供下面三项内容:
-
一个私有的数据域
-
一个公有的域访问器方法
-
一个公有的域更改器方法。
不要编写返回引用可变对象的访问器方法
如果需要返回一个可变数据域的拷贝,就应该使用clone
基于类的访问权限
方法可以访问所属类的私有特性(feature),而不仅限于访问隐式参数的私有特性。
final实例字段
可以将实例字段定义为final。构建对象时必须初始化这样的字段。必须确保在每一个构造器执行之后,这个字段的值被设置,并且在后面的操作中,不能够再对它进行修改。
但对于可变的类,使用final修饰符可能会造成混乱:
1 | private final StringBulider evaluations; |
final关键字只是表示存储在evaluations变量中的对象引用不会再指示其他StringBuilder对象。不过这个对象可以更改。
静态字段和静态方法
静态字段
如果将字段定义为static,这个字段不出现在每个类的对象中,每个静态字段只有一个副本,静态字段属于类,但不属于单个对象。
可以用来构造静态常量
1 | pubilc class Math{ |
静态方法
静态方法是一种不能向对象实施操作的方法。没有this参数,不能访问实例字段。
可用类名.静态方法名/对象.静态方法名调用。
工厂方法
继承子类
main方法
静态方法,不对任何对象进行操作
方法参数
一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。
Java程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,特别是,方法不能修改传递给它的任何参数变量的内容。
对于对象来说可行:
Java程序设计语言对对象采用的不是引用调用,实际上,对象引用是按值传递的。
- 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。
- 一个方法可以改变一个对象参数的状态。
- 一个方法不能让对象参数引用一个新的对象。
对象构造
重载
如果多个方法(比如,StringBuilder构造器方法)有相同的名字、不同的参数,便产生了重载。
Java允许重载任何方法,而不只是构造器方法。因此,要完整地描述一个方法,需要指出方法名以及参数类型。这叫做方法的签名(signature)。
不能有两个名字相同、参数类型也相同却返回不同类型值的方法。
默认字段初始化
必须明确地初始化方法中的局部变量。但是,如果没有初始化类中的字段,将会被自动初始化为默认值(0、false或null)。
无参数构造
一个类时没有编写构造器,那么系统就会提供一个无参数构造器。如果类中提供了至少一个构造器,但是没有提供无参数的构造器,则在构造对象时如果没有提供参数就会被视为不合法。
参数名技巧
参数变量会遮蔽同名的实例字段。
1 | public Employee(String aName,double aSalary){ |
调用另一个构造器
1 | public Employee(double s){ |
初始化块
1 | static |
对象析构
由于Java有自动的垃圾回收器,不需要人工回收内存,所以Java不支持析构器。
- 一旦用完就关闭,close方法
- 等到虚拟机退出再关闭,Euntime.addShutdownHook
记录
是特殊的类,状态不可改变,公共可读。
组件:一个记录的实例字段。
- toString
- equals
- hashCode
不能为记录增加实例字段
1 | record Piont(double x,double y){ |
记录的实例字段自动为final字段(可变),可自己设置不可变类型。
标准构造器:自动定义地设置所有实例字段的构造器。
自定义构造器:第一句必须调用另一个构造器,最终会调用标准构造器。
不能在简洁构造器主体中读取或修改实例字段。
包
类的导入
类可以使用所属包中的所有类和其他包的公共类。
访问其他包的公共类:
- 完全限定类名
- import java.time*(只能导入一个包)
在包中增加类
要想将一个类放入包中,就必须将包的名字放在源文件的开头,包中定义类的代码之前。
1 | package com.horstmann.corejava; |
如果没有在源文件中放置package语句,这个源文件中的类就被放置在一个无名包中。无名包是一个没有名字的包。
子目录:com\horstmann\corejava
编译器处理文件(不检查目录结构),解释器加载类
包访问
如果没有指定public或private,这个部分(类、方法或变量)可以被同一个包中的所有方法访问。
文档注释
javadoc——HTML文档,/**...*/
在标记之后紧跟着自由格式文本(free-form text)。标记由@开始,如@author或@param。自由格式文本的第一句应该是一个概要性的句子。javadoc实用程序自动地将这些句子抽取出来形成概要页。
要键入等宽代码,需使用{@code …}而不是<code>…</code>
果文档中有到其他文件的链接,例如,图像文件(用户界面的组件的图表或图像等),就应该将这些文件放到子目录doc-files中。javadoc实用程序将从源目录拷贝这些目录及其中的文件到文档目录中。在链接中需要使用doc-files目录,例如:<img src=“doc-files/uml.png”alt=“UML diagram”>。
-
类注释 类注释必须放在import语句之后,类定义之前。
-
方法注释 每一个方法注释必须放在所描述的方法之前。
-
@param变量描述
这个标记将对当前方法的“param”(参数)部分添加一个条目。这个描述可以占据多行,并可以使用HTML标记。一个方法的所有@param标记必须放在一起。
-
@return描述
这个标记将对当前方法添加“return”(返回)部分。这个描述可以跨越多行,并可以使用HTML标记。
-
@throws类描述
这个标记将添加一个注释,用于表示这个方法有可能抛出异常。
-
-
字段注释 只需要对公有字段(通常指的是静态常量)的注释
-
通用注释
-
@author姓名
-
@version文本
-
@since文本
-
@see引用
建立一个链接到com.horstmann.corejava.Employee类的raiseSalary(double)方法的超链接。
@see标记后面有一个<字符,就需要指定一个超链接。
-
@deprecated文本
-
-
包注释
- 提供一个以package.html命名的HTML文件。在标记<body>…</body>之间的所有文本都会被抽取出来。
- 提供一个以package-info.java命名的Java文件。这个文件必须包含一个初始的以/*和/界定的Javadoc注释,跟随在一个包语句之后。它不应该包含更多的代码或注释。
-
注释提取
-
切换到包含想要生成文档的源文件目录。
-
javadoc -d docDirectory nameOfPackage//包 javadoc -d docDirectory nameOfPackagenameOfPackage...//多个包的文档 javadoc -d docDirectory *java//文件在默认包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- 类设计技巧
- 一定要保证数据私有
- 一定要对数据初始化
- 不要在类中使用过多的基本类型
- 不是所有的字段都需要独立的字段访问器和字段更改器
- 将职责过多的类进行分解
- 类名和方法名要能够体现它们的职责
- 优先使用不可变的类
# 继承
## 类、超类和子类
“is-a”关系是继承的特征
### 定义子类
所有继承都是公共继承
```java
public class Manger extends Employee{
added methods and fields
}//Employee超类,基类,父类
-
声明为私有的类成员不会被这个类的子类继承,因为子类不能直接访问私有字段。但子类创建的对象有这些字段。
覆盖方法
1 | public double getSalary(){ |
super不是对象的引用,是指示编译器调用超类方法的特殊关键字。继承不会删除任何字段或方法。
子类构造器
1 | public Manager(String name, double salary, int year, int month, int day) |
如果子类的构造器没有显式地调用超类的构造器,则将自动地调用超类默认(没有参数)的构造器。
- this
- 引用隐式参数
- 调用该类其他的构造器
- super
- 调用超类的方法
- 调用超类的构造器
继承层次结构
由一个公共超类派生出来的所有类的集合被称为继承层次(inheritance hierarchy),继承层次中,从某个特定的类到其祖先的路径被称为该类的继承链(inheritance chain)。
Java不支持多继承。
多态
“is-a”规则的另一种表述法是置换法则。可以将一个子类的对象赋给超类变量。
1 | package inheritance; |
staff[0]的声明类型是Employee
理解方法调用
- 编译器查看对象的声明类型和方法名。
- 查看调用方法时提供的参数类型。如果在所有名为f的方法中存在一个与提供的参数类型完全匹配,就选择这个方法,这个过程被称为重载解析。
- 如果是private方法、static方法、final方法(有关final修饰符的含义将在下一节讲述)或者构造器,那么编译器将可以准确地知道应该调用哪个方法,我们将这种调用方式称为静态绑定(static binding)。与此对应的是,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定。
- 当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法。假设x的实际类型是D,它是C类的子类。如果D类定义了方法f(String),就直接调用它;否则,将在D类的超类中寻找f(String),以此类推。
虚拟机预先为每个类创建了一个方法表(method table),
在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。特别是,如果超类方法是public,子类方法一定要声明为public。
阻止继承:final类和方法
不允许扩展的类被称为final类,使用final修饰符,final类中的所有方法自动地成为final方法。
如果一个方法没有被覆盖并且很短,编译器就能够对它进行优化处理,这个过程为称为内联(inlining)。
枚举和记录总是final,它们不允许扩展。
强制类型转换✨
如果试图在继承链上进行向下的类型转换,并且“谎报”有关对象包含的内容,会报错
1 | Manager boss = (Manager) staff[1]; |
在进行类型转换之前,先查看一下是否能够成功地转换。这个过程简单地使用instanceof操作符就可以实现。如果这个类型转换不可能成功,编译器就不会进行这个转换。
- 只能在继承层次内进行类型转换。
- 在将超类转换成子类之前,应该使用instanceof进行检查。
instanceof模式匹配✨
Java16:直接在测试中声明子变量:
1 | if (staff[i] instanceof Manager boss){ |
staff[i]是Manager类的实体,boss变为staff[i];否则,不会设置boss,生成false值。
引入一个变量时,可以立即在同一个表达式中使用这个变量。
1 | Employee e; |
instanceof模式定义的局部变量会遮蔽字段。
受保护访问
希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个字段。为此,需要将这些方法或域声明为protected。受保护的字段只能由同一个包的类访问
🎀Java用于控制可见性的4个访问修饰符
- 仅对本类可见——private
- 对所有类可见——public
- 对本包和所有子类可见——protected
- 对本包可见——默认(很遗憾),不需要修饰符
Object:所有类的超类
可以使用Object类型的变量引用任何类型的对象:
1 | Object obj new Employee("Harry Hacker",35000); |
Object类型的变量只能用于作为各种值的泛型容器。要想对其中的内容进行具体的操作,还需要清楚对象的原始类型,并进行相应的类型转换。
只有基本类型(primitive types)不是对象,例如,数值、字符和布尔类型的值都不是对象。所有的数组类型,不管是对象数组还是基本类型的数组都扩展了Object类。
equals方法
只有在两个对象属于同一个类时,才有可能相等。
在子类中定义equals方法时,首先调用超类的equals。如果检测失败,对象就不可能相等。如果超类中的字段都相等,就需要比较子类中的实例字段。
相等测试与继承
如果发现类不匹配,equals方法就返回false。但是,许多程序员却喜欢使用instanceof进行检测:
1 | if(!(otherObject instanceof Employee)) return false; |
特性:
- 自反性:对于任何非空引用x, x.equals(x)应该返回true。
- 对称性:对于任何引用x和y,当且仅当y.equals(x)返回true,x.equals(y)也应该返回true。
- 传递性:对于任何引用x、y和z,如果x.equals(y)返回true,y.equals(z)返回true, x.equals(z)也应该返回true。
- 一致性:如果x和y引用的对象没有发生变化,反复调用x.equals(y)应该返回同样的结果。
- 对于任意非空引用x, x.equals(null)应该返回false。
注意:
- 如果子类能够拥有自己的相等概念,则对称性需求将强制采用getClass进行检测。
- 如果由超类决定相等的概念,那么就可以使用instanceof进行检测,这样可以在不同子类的对象之间进行相等的比较。
hashCode方法
要考虑这个散列码是基于数值还是地址计算的
toString方法
最好给每个类都搞一个toString方法。
Object类定义了toString方法,用来打印输出对象所属的类名和散列码。
泛型数组列表
在运行时确定数组大小——ArrayList,一个采用类型参数(type parameter)的泛型类(generic class)。
声明数组列表
1 | ArrayList<Employee> staff = new ArryList<Employee>(100);//初始容量100 |
分配数组列表 != 分配数组元素
确定数组大小不再变化后可使用trimToSize方法,需移动内存块。
访问数组元素
只有i小于或等于数组列表的大小时,才能够调用list.set(i, x),要得到一个数组列表的元素,可以使用:
1 | Employee e = staff.get(i); |
更为保险的方法是想创建一个数组列表,而后将其转换为数组元素(toArray方法)。
类型化与原始数组列表的兼容性
鉴于兼容性的考虑,编译器在对类型转换进行检查之后,如果没有发现违反规则的现象,就将所有的类型化数组列表转换成原始ArrayList对象。在程序运行时,所有的数组列表都是一样的,即没有虚拟机中的类型参数。因此,类型转换(ArrayList)和(ArrayList
只要在与遗留的代码进行交叉操作时,研究一下编译器的警告性提示,并确保这些警告不会造成太严重的后果就行了。
对象包装器与自动装箱
对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。
对象包装器类还是final,因此不能定义它们的子类。
1 | var List = new ArrayList<Integer>(); |
绝对不要依赖包装器对象的同一性。不要用
==
比较包装器对象(这是依据存储位置进行比较),可以用equals()比较,也不要将包装器对象作为锁。
装箱和拆箱是编译器的工作,不是虚拟机。
Integer对象是不可变的,在方法内部对这个拷贝所做的任何修改都不会影响到原始变量。这就是所谓的“按值传递”。
参数个数可变的方法
可以将已经存在且最后一个参数是数组的方法重新定义为可变参数的方法,而不会破坏任何已经存在的代码。
1 | public static void main(String...args) |
抽象类
包含一个或多个抽象方法的类本身必须被声明为抽象的,抽象方法相当于子类中实现的具体方法的占位符。
抽象类不能实例化,仍可以创建一个抽象类的对象变量,但只能引用非抽象子类的对象。
编译器只允许调用在类中声明的方法。
1 | public acstract class Person{ |
枚举类
枚举的构造器总是私有的,所有枚举类型都是抽象类Enum的子类,
- ordinal方法返回enum声明中枚举常量的位置,位置从0开始计数。
- 每个枚举类型都有一个静态的values方法,它将返回一个包含全部枚举值的数组。
- toString,这个方法能够返回枚举常量名。
密封类
acstract sealsd声明为密封类,控制哪些类可以继承它。
一个密封类允许的子类必须是可访问的,不能是嵌套在另一个类中的私有类,也不能是位于另一个包中的包可见的类。
记录和枚举可以实现接口但不能扩展类
反射(待补充)
能够分析类能力的程序称为反射。
接口、lambda表达式和内部类
接口(interface)技术,这种技术主要用来描述类具有什么功能,而并不给出每个功能的具体实现。
使用lambda表达式,可以用一种精巧而简洁的方式表示使用回调或变量行为的代码。
内部类(inner class)机制,内部类定义在另外一个类的内部,其中的方法可以访问包含它们的外部类的字段。内部类技术主要用于设计具有相互协作关系的类集合。
代理是一种非常专业的构造工具,它可以用来构建系统级的工具。
接口
接口概念
接口(英文:Interface),在JAVA编程语言中是一个抽象类型,是抽象方法的集合,接口通常以interface来声明。一个类通过继承接口的方式,从而来继承接口的抽象方法。
接口并不是类,编写接口的方式和类很相似,但是它们属于不同的概念。类描述对象的属性和方法。接口则包含类要实现的方法。
除非实现接口的类是抽象类,否则该类要定义接口中的所有方法。
接口无法被实例化,但是可以被实现。一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类。另外,在 Java 中,接口类型可用来声明一个变量,他们可以成为一个空指针,或是被绑定在一个以此接口实现的对象。
接口与类相似点
- 一个接口可以有多个方法。
- 接口文件保存在 .java 结尾的文件中,文件名使用接口名。
- 接口的字节码文件保存在 .class 结尾的文件中。
- 接口相应的字节码文件必须在与包名称相匹配的目录结构中。
接口与类的区别
- 接口不能用于实例化对象。
- 接口没有构造方法。
- 接口中所有的方法必须是抽象方法,Java 8 之后 接口中可以使用 default 关键字修饰的非抽象方法。
- 接口不能包含成员变量,除了 static 和 final 变量。
- 接口不是被类继承了,而是要被类实现。
- 接口支持多继承。
接口特性
- 接口中每一个方法也是隐式抽象的,接口中的方法会被隐式的指定为 public abstract(只能是 public abstract,其他修饰符都会报错)。
- 接口中可以含有变量,但是接口中的变量会被隐式的指定为 public static final 变量(并且只能是 public,用 private 修饰会报编译错误)。
- 接口中的方法是不能在接口中实现的,只能由实现接口的类来实现接口中的方法。
抽象类和接口的区别
- 抽象类中的方法可以有方法体,就是能实现方法的具体功能,但是接口中的方法不行。
- 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的。
- 接口中不能含有静态代码块以及静态方法(用 static 修饰的方法),而抽象类是可以有静态代码块和静态方法。
- 一个类只能继承一个抽象类,而一个类却可以实现多个接口。
接口的声明
1 | interface 接口名称 [extends 其他的接口名] { |
Interface关键字用来声明一个接口。
- 接口是隐式抽象的,当声明一个接口的时候,不必使用abstract关键字。
- 接口中每一个方法也是隐式抽象的,声明时同样不需要abstract关键字。
- 接口中的方法都是公有的。
接口的实现
当类实现接口的时候,类要实现接口中所有的方法。否则,类必须声明为抽象的类。
类使用implements关键字实现接口。在类声明中,Implements关键字放在class声明后面。
实现一个接口的语法:
1 | ...implements 接口名称[, 其他接口名称, 其他接口名称..., ...] ... |
EG:
1 | /* 文件名 : MammalInt.java */ |
重写接口中声明的方法时,需要注意以下规则:
- 类在实现接口的方法时,不能抛出强制性异常,只能在接口中,或者继承接口的抽象类中抛出该强制性异常。
- 类在重写方法时要保持一致的方法名,并且应该保持相同或者相兼容的返回值类型。
- 如果实现接口的类是抽象类,那么就没必要实现该接口的方法。
在实现接口的时候,也要注意一些规则:
- 一个类可以同时实现多个接口。
- 一个类只能继承一个类,但是能实现多个接口。
- 一个接口能继承另一个接口,这和类之间的继承比较相似。
接口的继承
一个接口能继承另一个接口,和类之间的继承方式比较相似。接口的继承使用extends关键字,子接口继承父接口的方法。
下面的Sports接口被Hockey和Football接口继承:
1 | // 文件名: Sports.java |
Hockey接口自己声明了四个方法,从Sports接口继承了两个方法,这样,实现Hockey接口的类需要实现六个方法。
相似的,实现Football接口的类需要实现五个方法,其中两个来自于Sports接口。
接口的多继承
在Java中,类的多继承是不合法,但接口允许多继承。
在接口的多继承中extends关键字只需要使用一次,在其后跟着继承接口。 如下所示:
1 | public interface Hockey extends Sports, Event |
与类不同的是,接口允许多继承,而 Sports及 Event 可以定义或是继承相同的方法。
标记接口
最常用的继承接口是没有包含任何方法的接口。
标记接口是没有任何方法和属性的接口.它仅仅表明它的类属于一个特定的类型,供其他代码来测试允许做一些事情。
标记接口作用:简单形象的说就是给某个对象打个标(盖个戳),使对象拥有某个或某些特权。
例如:java.awt.event 包中的 MouseListener 接口继承的 java.util.EventListener 接口定义如下:
1 | package java.util; |
没有任何方法的接口被称为标记接口。标记接口主要用于以下两种目的:
-
建立一个公共的父接口:
正如EventListener接口,这是由几十个其他接口扩展的Java API,你可以使用一个标记接口来建立一组接口的父接口。例如:当一个接口继承了EventListener接口,Java虚拟机(JVM)就知道该接口将要被用于一个事件的代理方案。
-
向一个类添加数据类型:
这种情况是标记接口最初的目的,实现标记接口的类不需要定义任何接口方法(因为标记接口根本就没有方法),但是该类通过多态性变成一个接口类型。