Java核心卷Ⅰ(原书第10版)笔记(上)
文章目录
JDK 版本区别
- JDK1.4: 正则表达式,异常链,NIO,日志类,XML解析器,XLST转换器
- JDK1.5: 自动装箱、泛型、动态注解、枚举、可变长参数、遍历循环
- JDK1.6: 提供动态语言支持、提供编译API和卫星HTTP服务器API,改进JVM的锁,同步垃圾回收,类加载
- JDK1.7: 提供GI收集器、加强对非Java语言的调用支持(JSR-292,升级类加载架构
- JDK1.8: Lambda 表达式、方法引用、默认方法、新工具、Stream API、Date Time API 、Optional 类、Nashorn, JavaScript 引擎
书中知识累计
- NIO:
(JDK 1.4) 同步非阻塞IO,主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(多路复用器)。 - Object.clone():
(JDK 1.4)1.4 之前,clone
方法的返回类型总是Object
, 而现在可以为你的clone
方法指定正确的返回类型。 - 参数数量可变:
(JDK 5.0)System.out.printf(String fmt , Object... args);
// 如果args是整型数组或基本类型的值,将被自动装箱。 - StringBuilder:
(JDK 5.0) 引入StringBuilder
。 - 泛型:
(JDK 5.0) 部分接口在此之后被改进为泛型类型,如Comparable<T>
。 - switch:
(JDK 1.7) 开始支持String
,其实也是语法糖。 - 泛型自动识别:
(JDK 1.7)ArrayList<Employee> staff = new ArrayList<>();
// PS: 泛型是Java SE 5.0出现的。 - 同一个catch子句捕获多个异常类型:
(JDK 1.7) 这些异常处理动作相同,捕获多个异常时,异常变量隐含为final
变量,也就是不能改e
的值。try{...}catch (FileNotFoundException | UnknownHostException e){...} // 这些异常处理动作相同
- 接口_实现方法:
(JDK 1.8)可以在接口中提供简单方法了,当然,这些方法不能引用实例域,因为接口没有实例。 - 接口_静态方法:
(JDK 1.8)可以在接口中添加静态方法,只是这有违于将接口作为抽象规范的初衷。 - 接口_默认方法:
(JDK 1.8)可以在接口中添加默认方法。 - lambda表达式:
(JDK 1.8)引入 lambda 表达式。
第3章 Java的基本程序设计结构
3.6.6 码点与代码单元
-
要想得到实际的长度,即码点数量,例如:
int cpCount = greeting.codePointCount(0, greeting.length());
-
调用
s.charAt(n)
将返回位置 n 的代码单元,n 介于 0 ~ s.length()-1 之间。
char first = greeting.charAt(0); // first is 'H'
char last = greeting.charAt(4); // last is ’o’
-
要想得到第 i 个码点,应该使用下列语句:
int index = greeting.offsetByCodePoints(0,i);
int cp = greeting.codePointAt(index);</code>
-
想要获取字符串中的所有码点:
int[] codePoints = str.codePointsO.toArray();
-
反之,要把一个码点数组转换为一个字符串:
String str = new String(codePoints, 0, codePoints.length) ;
3.8.6 中断控制流程语句
- Java 提供了一种带标签的 break语句, 用于跳出多重嵌套的循环语句。请注意,标签必须放在希望跳出的最外层循环之前,并且必须紧跟一个冒号。事实上,可以将标签应用到任何语句中,甚至可以应用到if语句或者块语句中,当然,并不提倡使用这种方式。 另外需要注意, 只能跳出语句块,而不能跳入语句块。
用例:
read_data:
while (. . .) // this loop statement is tagged with the label
{
...
for (. . .) // this inner loop is not labeled
{
Systen.out.print("Enter a number >= 0: ");
n = in.nextlnt();
if (n < 0) // should never happen-can’t go on
break read_data;
// break out of readjata loop
...
}
}
第4章 对象与类
4.1.2 对象
对象的三个特性:
- 对象的行为(behavior):可以对对象施加哪些操作,或可以对对象施加哪些方法?【方法】
- 对象的状态(state):当施加那些方法时,对象如何响应?【值】
- 对象标识(identity):如何辨别具有相同行为与状态的不同对象?【HASH/EQUALS】
4.1.4 类之间的关系
在类之间,最常见的关系:
- 依赖(“uses-a”)
- 聚合(“has-a”)
- 继承(“is-a”)
4.2.2 Java类库中的LocalDate类
部分常用API如下:
LocalDate.now(); // 获得当前实际
LocalDate.of(1999, 12, 31); // 可以提供年、 月和日来构造对应一个特定日期的对象
int year = newYearsEve.getYearO; // 1999
int month = newYearsEve.getMonthValueO; // 12
int day = newYearsEve.getDayOfMonth(); // 31
LocalDate aThousandDaysLater = newYearsEve.piusDays(1000): // 加1000天,并生成新对象
与常用的时间类型互转:
// 01. java.util.Date --> java.time.LocalDateTime
public void UDateToLocalDateTime() {
java.util.Date date = new java.util.Date();
Instant instant = date.toInstant();
ZoneId zone = ZoneId.systemDefault();
LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, zone);
}
// 02. java.util.Date --> java.time.LocalDate
public void UDateToLocalDate() {
java.util.Date date = new java.util.Date();
Instant instant = date.toInstant();
ZoneId zone = ZoneId.systemDefault();
LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, zone);
LocalDate localDate = localDateTime.toLocalDate();
}
// 03. java.util.Date --> java.time.LocalTime
public void UDateToLocalTime() {
java.util.Date date = new java.util.Date();
Instant instant = date.toInstant();
ZoneId zone = ZoneId.systemDefault();
LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, zone);
LocalTime localTime = localDateTime.toLocalTime();
}
// 04. java.time.LocalDateTime --> java.util.Date
public void LocalDateTimeToUdate() {
LocalDateTime localDateTime = LocalDateTime.now();
ZoneId zone = ZoneId.systemDefault();
Instant instant = localDateTime.atZone(zone).toInstant();
java.util.Date date = Date.from(instant);
}
// 05. java.time.LocalDate --> java.util.Date
public void LocalDateToUdate() {
LocalDate localDate = LocalDate.now();
ZoneId zone = ZoneId.systemDefault();
Instant instant = localDate.atStartOfDay().atZone(zone).toInstant();
java.util.Date date = Date.from(instant);
}
// 06. java.time.LocalTime --> java.util.Date
public void LocalTimeToUdate() {
LocalTime localTime = LocalTime.now();
LocalDate localDate = LocalDate.now();
LocalDateTime localDateTime = LocalDateTime.of(localDate, localTime);
ZoneId zone = ZoneId.systemDefault();
Instant instant = localDateTime.atZone(zone).toInstant();
java.util.Date date = Date.from(instant);
}
4.3.7 基于类的访问权限
如果需要返回一个可变对象的引用,应该首先对它进行克隆(clone)。【分为浅拷贝、深拷贝,拷贝应考虑创建对象的开销】
4.4.2 静态常量
如果查看一下
System
类, 就会发现有一个setOut
方法,它可以将System.out
【public static final PrintStream out】设置为不同的流。读者可能会感到奇怪,为什么这个方法可以修改 final 变量的值。原因在于, setOut 方法是一个本地方法,而不是用 Java 语言实现的。本地方法可以绕过 Java 语言的存取控制机制。这是一种特殊的方法,在自己编写程序时,不应该这样处理。
4.4.4 工厂方法
构造器缺陷:
• 无法命名构造器。构造器的名字必须与类名相同。
• 当使用构造器时,无法改变所构造的对象类型。而 Factory方法可以返回其子类或实现类。
• 其他细节,回去翻 《Effective Java》。
4.6.1 重载
Java 允许重载任何方法, 而不只是构造器方法。因此,要完整地描述一个方法,
需要指出方法名以及参数类型。这叫做方法的签名(signature)。 例如, String 类有 4 个
称为 indexOf 的公有方法。 它们的签名是
indexOf(int) indexOf(int, int) indexOf(String) indexOf(String, int)
返回类型不是方法签名的一部分。 也就是说, 不能有两个名字相同、 参数类型也相
同却返回不同类型值的方法。
4.6.7 初始化块
类的构造顺序(从上至下)
- 静态初始化
- 父类静态成员和static块
- 子类静态成员和static块
- 父类初始化
- 父类普通成员和非static块
- 父类构造函数
- 子类初始化
- 子类普通成员和非static块
- 子类构造函数
4.6.8 对象析构与finalize方法
不要依赖于使用 finalize 方法回收任何短缺的资源, 这是因为很难知道这个方法什么时候才能够调用。有个名为
System.mnFinalizersOnExit(true)
的方法能够确保 finalizer 方法在 Java 关闭前被调用。不过,这个方法并不安全,也不鼓励大家使用。有一种代替的方法是使用方法Runtime.addShutdownHook
添加“关闭钓” (shutdown hook), 详细内容请参看 API 文档。具体可以查看 《Effective Java》
4.10 类设计技巧
- 一定要保证数据私有;
- 一定要对数据初始化;
- 不要在类中使用过多的基本类型;
- 不是所有的域都需要独立的域访问器和域更改器;
- 将职责过多的类进行分解;
- 类名和方法名要能够体现它们的职责;
- 优先使用不可变的类;
第5章 继承
5.1.5 多态
在 Java 中,子类数组的引用可以转换成超类数组的引用, 而不需要采用强制类型转换。但这存在一个问题。
例如:
Child[] childArr = new Child[10];
Father[] fatherArr = childArr;
注意,此时父类数组与子类数组公用一个对象。
如果此时我们存入一个父类对象,并在子类数组中调用子类特有方法,将会导致调用一个不存在的实例域,进而搅乱相邻存储空间的内容。
例如:
fatherArr[0] = new FatherArr("Harry Hacker", . . .); // throw ArrayStoreException
childArr[0].childMethod();
· 以上代码编译时不会报错,运行时就会引发ArrayStoreException
异常,因为开发人员试图存储一个父类类型的引用。
5.1.6 理解方法调用
如果是private
方法、static
方法、final
方法或者构造器
,那么编译器将可以准确地知道应该调用哪个方法,我们将这种调用方式称为静态绑定(static binding)。与之相对的是动态绑定,流程如下:
- 重载/重写时:如果编译器能找到与之对应的方法签名则调用成功,如果编译器没有找到与参数类型匹配的方法,或者发现经过类型转换后有多个方法与之匹配(
int
可以转换成double/float
等),就报错。 - 方法表:每次调用方法都要进行搜索,时间开销相当大。因此, 虚拟机预先为每个类创建了一个方法表( method table), 其中列出了所有方法的签名和实际调用的方法。这样一来,在真正调用方法的时候,虚拟机仅查找这个表就行了。
- 虚拟机解析
e.getSalary()
方法的过程为:- 首先,虚拟机提取
e
的实际类型的方法表。可能是当前对象类型或其子类
的方法表。 - 然后,虚拟机搜索定义
getSalary
签名的类。此时,虚拟机已经知道应该调用哪个方法。 - 最后,虚拟机调用方法。
- 首先,虚拟机提取
5.1.8 强制类型转换
如果将一个类声明为
final
,只有其中的方法自动地成为final
,而不包括域。
如果一个方法没有被覆盖并且很短,编译器就能够对它进行优化处理,这个过程为称为内联(inlining)。例如,内联调用
e.getName()
将被替换为访问e.name
域。
5.2 Object:所有类的超类
所有的数组类型,不管是对象数组还是基本类型的数组都扩展了Object
类。
Employee[] staff = new Employee[10];
Object obj = staff; // OK,PS:"参数数量可变"与此特性有关,System.out.printf();
obj = new int[10]; // OK
5.2.1 equals方法
equals
方法需要具有的特性:
- 自反性:对于任何非空引用 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.eqimIS(y)
应该返回同样的结果。 - 非空性: 对于任意非空引用 x,
x.equals(null)
应该返回 false。
getClass()
与instanceof
的选择
// 如果子类能够拥有自己的相等概念, 则对称性需求将强制采用 getClass 进行检测。
if (getClass() != otherObject.getClass()) return false;
// 如果由超类决定相等的概念,那么就可以使用 instanceof 进行检测,这样可以在不同子类的对象之间进行相等的比较。
if (!(otherObject instanceof Employee)) return false;
// 考虑以下用例情况
Animal animal = new Animal();
Dog dog = new Dog(); // Dog extend Animal
animal.equals(dog); // dog instanceof Animal #true
dog.equals(animal); // animal instanceof Dog #false
某些书的作者认为不应该利用 getClass 检测, 因为这样不符合置换原则有一个应用 AbstractSet 类的 equals 方法的典型例子,它将检测两个集合是否有相同的元素。 AbstractSet 类有两个具体子类:TreeSet 和 HashSet, 它们分别使用不同的算法实现查找集合元素的操作。无论集合采用何种方式实现, 都需要拥有对任意两个集合进行比较的功能。
然而, 集合是相当特殊的一个例子, 应该将 AbstractSetequals 声明为 final, 这是因为没有任何一个子类需要重定义集合是否相等的语义 (事实上, 这个方法并没有被声明为 final。这样做, 可以让子类选择更加有效的算法对集合进行是否相等的检测)
5.3 泛型数组列表
ArrayList
API
// ensureCapacity 方法确保数组列表在不重新分配存储空间的情况下就能够保存给定数量的元素。
ArrayList<Employee> staff = new ArrayList<>();
staff.ensureCapacity(lOO); // 等价于(还不如写成) ArrayList<Employee> staff = new ArrayList<>(lOO);
// 一旦能够确认数组列表的大小不再发生变化, 就可以调用 trimToSize 方法。这个方法将存储区域的大小调整为当前元素数量所需要的存储空间数目。垃圾回收器将回收多余的存储空间。
// 一旦整理了数组列表的大小,添加新元素就需要花时间再次移动存储块,所以应该在确认不会添加任何元素,再调用。
staff.trimToSize(); // 感觉用的机会不多
5.4 对象包装器与自动装箱
- 自动装箱规范要求
boolean
、byte
、char
<= 127,介于-128 ~ 127之间的short
和int
被包装到固定的对象中(也就是这个范围内的对象都会用到固定的装箱类型可用==
判断,但还是应使用equals
,个人认为是考虑到此范围对象可能存在频繁装拆箱子问题吧)。
Byte b1 = 0101;
Byte b2 = 0101;
System.out.println(b1 == b2); // true
Boolean bl1 = true;
Boolean bl2 = true;
System.out.println(bl1 == bl2); // true
Integer in1 = 127;
Integer in2 = 127;
System.out.println(in1 == in2); // true
Integer in1 = 128;
Integer in2 = 128;
System.out.println(in1 == in2); // false
- 自动拆箱可能会有空指针(
NullPointerException
)问题。 - 装箱和拆箱是编译器认可的,而不是虚拟机。编译器在生成类的字节码时,插入必要的方法调用。虚拟机只是执行这些字节码
5.6 枚举类
枚举的实例是确定的,不需要调用 equals, 直接使用 “==”。
5.7.1 Class类
- 一个 Class 对象实际上表示的是一个类型,而这个类型未必一定是一种类。 例如,
int
不是类,但int.class
是一个Class
类型的对象。 Class
类实际上是一个泛型类。例如,Employee.class
的类型是Class<Employee>
。没有说明这个问题的原因是:它将已经抽象的概念更加复杂化了。在大多数实际问题中,可以忽略类型参数,而使用原始的Class
类。- 鉴于历史原
getName
方法在应用于数组类型的时候会返回一个很奇怪的名字:Double[].class.getName()
返回“[Ljava.lang.Double”;int[].class.getName()
返回“ [I”;
- 虚拟机为每个类型管理一个
Class
对象。因此,可以利用==
运算符实现两个类对象比较的操作。
5.7.3 利用反射分析类的能力
Java.lang.reflect
包中有三个类Field
、Method
和Constructor
分别用于描述类的域、方法和构造器。这三个类还有一个叫做getModifiers
的方法,可以利用java.lang.reflect
包中的Modifier
类的静态方法分析getModifiers
返回的整型数值。(isPublic
、isPrivate
或isFinal
等)判断方法或构造器是否是public
、private
或final
等其他标识.
Class
类中的getFields
、getMethods
和getConstructors
方法将分别返回类提供的public域、方法和构造器数组,其中包括超类的公有成员。Class
类的getDeclareFields
、getDeclareMethods
和getDeclaredConstructors
方法将分别返回类中声明的全部域、方法和构造器,其中包括私有和受保护成员,但不包括超类的成员。
5.7.4 在运行时使用反射分析对象
setAccessible
方法是AccessibleObject
类中的一个方法, 它是 Field、 Method 和 Constructor 类的公共超类。这个特性是为调试、持久存储和相似机制提供的。通过此方法可以设置访问控制(访问私有方法、对象)。
5.7.5 使用反射编写泛型数组代码
java.lang.reflect
包中Array
类里有许多关于数组的反射方法,以下为拷贝数组并拓展的演示:
public static Object goodCopyOf(Object a, int newLength)
{
Class cl = a.getClass();
if (Icl.isArray()) return null ;
Class componentType = cl.getComponentType();
// 获得数组长度
int length = Array.getLength(a);
// 创建泛型数组,PS:如果创建Object数组,下方的拷贝数组后,转类型会报错。因为新数组的真实类型与a类型不符。
Object newArray = Array.newlnstance(componentType, newLength);
// 拷贝数组
System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength));
return newArray;
}
// PS: 以上做的操作基本就是 java.util.Arrays 中的 copyOf 的实现,有兴趣可以看看源码【JDK 1.8.0_121】:
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
5.7.6 调用任意方法
invoke
的参数和返回值必须是Object
类型的,这就意味着必须进行多次的类型转换。这样做将会使编译器错过检查代码的机会。反射获得方法指针的代码要比仅仅直接调用方法明显慢一些。有鉴于此,建议仅在必要的时候才使用Method
对象,而最好使用接口以及 Java SE 8中的lambda 表达式
(第 6 章中介绍)。 特别要重申: 建议 Java 开发者不要使用Method
对象的回调功能。使用接口进行回调会使得代码的执行速度更快, 更易于维护。
- 如果方法返回的是基本类型如
double
,返回的对象实际上是一个Double
, 必须相应地完成类型转换。可以使用自动拆箱将它转换为一个 double。double s = (Double) m2.invoke("harry");
// m2是一个返回double的方法。
getMethod
方法可以准确的获得方法(有参数要给类型),如同getField
方法。Method m2 = Employee.class.getMethod("raiseSalary", double.class);
- 静态方法的调用
double y = (Double) f.invoke(null, x);
// 静态方法第一个参数传的是 null
5.8 继承的设计技巧
- 将公共操作和域放在超类;
- 不要使用受保护的域(protected);
- 使用继承实现“is-a” 关系;
- 除非所有继承的方法都有意义,否则不要使用继承;
- 在覆盖方法时,不要改变预期的行为(在覆盖子类中的方法时,不要偏离最初的设计
想法); - 使用多态,而非类型信息;
- 不要过多地使用反射(反射是很脆弱的,即编译器很难帮助人们发现程序中的错误);
第6章 接口、lambda表达式与内部类
6.1.1 接口概念
-
接口中的所有方法自动地属于
public
。因此,在接口中声明方法时,不必提供关键字public
。不过,在实现接口时,必须把方法声明为public
,否则,编译器将认为这个方法的访问属性是包可见性,即类的默认访问属性,之后编译器就会给出试图提供更严格的访问权限的警告信息。 -
对比数值大小应避免使用减法,以免造成减法运算的溢出,或两个很接近但又不相等浮点值的差,经过四舍五入后又可能变成 0 的问题。应使用数值包装类的
compare
方法,如Integer.compare(int x , int y)
。 -
如果父类实现了Comparable方法,子类覆盖compareTo方法时就必须要有父子类比较的准备,如果子类之间的比较含义不一样,那就属于不同类对象的非法比较。每个
compareTo
方法都应该在开始时进行下列检测:
if (getClass() != other.getClass()) throw new ClassCastException();
如果存在这样一种通用算法,它能够对两个不同的子类对象进行比较,则应该在超类中提供一个compareTo
方法,并将这个方法声明为final
。 -
可以使用
instanceof
判断一个对象是否实现了某个特定接口或类if (anObject instanceof Comparable) { . . . }
-
在接口中不能包含实例域或静态方法(JDK 1.8 中,允许再接口中添加静态方法与默认方法),但却可以包含常量,与接口中的方法都自动地被设置为
public
—样, 接口中的域将被自动设为public static final
:
public interface Powered extends Moveable
{
double milesPerCallonO;
double SPEED_LIHIT = 95; // a public static final constant
}
可以将接口方法标记为
public
, 将域标记为public static final
。有些程序员出于习惯或提高清晰度的考虑, 愿意这样做。但 Java 语言规范却建议不要书写这些多余的关键字, 本书也采纳了这个建议。
有些接口只定义了常量,而没有定义方法。例如,在标准库中有一个
SwingConstants
就是这样一个接口, 其中只包含 NORTH、 SOUTH 和 HORIZONTAL 等常量。 任何实现 SwingConstants 接口的类都自动地继承了这些常量, 并可以在方法中直接地引用 NORTH,而不必采用 SwingConstants.NORTH 这样的繁琐书写形式。然而,这样应用接口似乎有点偏离了接口概念的初衷, 最好不要这样使用它。
6.1.4 静态方法
在 Java SE 8 中,允许在接口中增加静态方法。理论上讲,没有任何理由认为这是不合法的。 只是这有违于将接口作为抽象规范的初衷。
目前为止, 通常的做法都是将静态方法放在伴随类中。在标准库中,你会看到成对出现的接口和实用工具类, 如 Collection/Collections 或 Path/Paths。(PS: 原来这样的规律鸭)但是实现你自己的接口时,不再需要为实用工具方法另外提供一个伴随类。JavaSE 8 中,这个技术已经过时。现在可以直接在接口中实现方法。
6.1.5 默认方法
在Java SE 8 中,可以使用
default
修饰符标记一个方法为默认方法,当然可以把所有方法声明为默认方法,而这些默认方法什么也不做(PS: 例如鼠标事件,我只想实现单即事件,双击之类的事件什么都不做)。这样一来,实现这个接口的程序员只需要为他们真正关心的事件覆盖相应的监听器。默认方法可以调用任何其他方法。例如,Collection
接口可以定义一个便利方法:
public interface Collection
{
int size(); // An abstract method
// 这样实现 Collection 的程序员就不用操心实现 isEmpty 方法了。
default boolean isEmpty(){
return size() == 0;
}
. . .
}
接口演化
默认方法的一个重要用法是“接口演化”(interface evolution)。以
Collection
接口为例,这个接口作为 Java 的一部分已经有很多年了。假设你很久之前的一个Bag
类实现了Collection接口,后来在Java SE 8 中,此接口又增加了一个stream
方法,而 stream 方法并非默认方法。那么 Bag 类将不能编译, 因为它没有实现这个新方法。为接口增加一个非默认方法不能保证“源代码兼容”(source compatible)。
不过, 假设不重新编译这个类, 而只是使用原先的一个包含这个类的 JAR 文件。这个类仍能正常加载,尽管没有这个新方法。程序仍然可以正常构造 Bag 实例, 不会有意外发生。
( 为接口增加方法可以保证“ 二进制兼容”)。不过, 如果程序在一个 Bag 实例上调用 stream
方法,就会出现一个AbstractMethodError
。
将方法实现为一个默认方法就可以解决这两个问题。Bag 类又能正常编译了。另外如果没有重新编译而直接加载这个类, 并在一个 Bag 实例上调用 stream 方法, 将调用
Collection.stream
方法
解决默认方法冲突
- 规则如下:
- 超类优先。如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略(所以用默认方法重写定义Object类中的某个方法是没有意义的)。
- 接口冲突。 如果一个超接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型 (不论是否是默认参数)相同的方法, 必须覆盖这个方法来解决冲突(也就是子类必须实现这个方法)。
6.3 lambda表达式
阅读本书之前已使用 JDK 8开发过一段时间,对于lambda的使用部分并不陌生,以下博主有详细的语法详解。更多有趣的内容在卷Ⅱ中。
lambda表达式详解
Java 8系列之Stream的基本语法详解
使用 lambda 表达式的重点是延迟执行 (deferred execution) 毕竟,如果想耍立即执行代码,完全可以直接执行,而无需把它包装在一个lambda表达式中。之所以希望以后再执行代码,这有很多原因,如:
- 在一个单独的线程中运行代码;
- 多次运行代码;
- 在算法的适当位置运行代码(例如,排序中的比较操作);
- 发生某种情况时执行代码 (如,点击了一个按钮,数据到达,等等);
- 只在必要时才运行代码;
常用函数式接口
函数是接口 | 参数类型 | 返回类型 | 抽象方法名 | 描述 | 其他方法 |
---|---|---|---|---|---|
Runnable | 无 | void | run | 作为无参数或返回值的动作运行 | |
Supplier<T> | 无 | T | get | 提供一个 T 类型的值 | |
Consumer<T> | T | void | accept | 处理一个 T 类型的值 | andThen |
BiConsumer<T,U> | T,U | void | accept | 处理 T 和 U 类型的值 | andThen |
Function<T,R> | T | R | apply | 有一个 T 类M参数的函数 | compose, andThen, identity |
BiFunction<T,U,R> | T,U | R | apply | 有 T 和 U 类型.参数的函数 | andThen |
UnaryOperator<T> | T | T | apply | 类型 T 上的一元操作符 | compose, andThen, identity |
BinaryOperator<T> | T,T | T | apply | 类型 T 上的二元操作符 | andThen, maxBy, minBy |
Predicate<T> | T | boolean | test | 布尔值函数 | and, or, negate, isEqual |
BiPredicate<T, U> | T,U | boolean | test | 有两个参数的布尔值函数 | and, or, negate |
基本类型的函数式接口
函数是接口 | 参数类型 | 返回类型 | 抽象方法名 |
---|---|---|---|
BooleanSupplier | none | boolean | getAsBoolean |
P Supplier | none | P | getAsP |
P Consumer | P | void | accept |
ObjP Consumer<T> | T,P | void | accept |
P Function<T> | P | T | apply |
P ToQ Function | P | q | applyAsQ |
ToP Function<T> | T | P | applyAsP |
ToP BiFunction<T,U> | T,U | P | applyAsP |
P UnaryOperator | P | P | applyAsP |
P BnaryOperator | P,P | P | applyAsP |
P Pedicate | P | boolean | test |
注:p,q 为 int, long, double; P,Q 为 Int, Long, Double
6.4 内部类
- 内部类
- 内部类拥有外围类的引用参数。(外围类实例内部类时,默认的构造参数会带上外围类的引用,代码由编译器生成)。
- 只有内部类可以是私有类,而常规类只可以具有包可见性,或公有可见性。
- 内部类中声明的所有静态域都必须是
final
,因为一个静态域只有一个实例,不过对于每个外部对象, 会分别有一个单独的内部类实例。如果这个域不是final
, 它可能就不是唯一的。 - 内部类最好不能有
static
方法(静态内部类除外),虽然Java允许内部类拥有静态方法,但只能访问外围类的静态域和方法。Java 设计者认为相对于这种复杂性来说,它带来的好处有些得不偿失。 - 外部类与内部类名称之间使用
$
符号分割,比如:ClassName$InnerClassName
。
- 局部内部类
- 在方法中定义的类。
- 局部类不能用
public
或private
访问说明符进行声明。它的作用域被限定在声明这个局部类的块中,只有此块范围内的对象知道它的存在。 - 优点:局部类有一个优势,即对外部世界可以完全地隐藏起来,即便是单前方法的持有类也不能访问它。
- 优点:与其他内部类相比较,局部类不仅能够访问包含它们的外部类,还可以访问局部变量。不过,那些局部变量必须事实上为
final
(也是编译器处理的,它将局部变量带入构造器中,并存入一个final
域中,因为在延迟调用内部类代码时又需要使用外部类局部变量时,此时局部变量已不存在)。 - 在 JavaSE 8 之前, 必须把从局部类访问的局部变量声明为
final
,但可能遇到一些问题,比如局部变量是个计数器。补救方法是使用一个长度为 1 的数组。
- 匿名内部类
- 直接
new
创建一个实现 XX 接口/抽象类的新对象。ActionListener listener = new ActionListener() {...}
- 多年来,Java 程序员习惯的做法是用匿名内部类实现事件监听器和其他回调。如今最好还是使用
lambda
表达式。 - “双括号初始化”(double brace initialization),这里利用了内部类的语法,注意这里的双括号。 外层括号建立了
ArrayList
的一个匿名子类。 内层括号则是一个对象构造块(见第 4 章)。invite(new ArrayList<String>() {{ add("Harry"); add("Tony"); }});
PS:本人在实际编码时,SonarQube会提示“坏味道”,不过确实很方便。 - 生成曰志或调试消息时,通常希望包含当前类的类名,如:
Systen.err.println("Something awful happened in " + getClass());
不过,这对于静态方法不奏效。毕竟,调用getClass
时调用的是this.getClass()
, 而静态方法没有this
。所以应该使用以下表达式,在这里,newObject(){}
会建立Object
的一个匿名子类的一个匿名对象,getEnclosingClass
则得到其外围类,也就是包含这个静态方法的类:new Object(){}.getClass().getEnclosingClass() // gets class of static method
- 直接
- 静态内部类
- 有时候,使用内部类只是为了把一个类隐藏在另外一个类的内部,并不需要内部类引用外围类对象。为此,可以将内部类声明为
static
, 以便取消产生的引用(只有内部类可以声明为static
。静态内部类的对象除了没有对生成它的外围类对象的引用特权外,与其他所有内部类完全一样。)。 - 如果内部类对象是在静态方放中构造的,必须使用静态内部类,否则编译器会报错:没有可用的隐式外部类对象初始化内部类对象(静态方法没有外部实例的引用,自然也无法通过外部实例引用初始化内部类【内部类 第一条】)。
- 与常规内部类不同,静态内部类可以有静态域和方法(【内部类 第四条】)。
- 声明在接口中的内部类自动成为
static
和public
类(跟JDK 1.8的接口新特性有关)。
- 有时候,使用内部类只是为了把一个类隐藏在另外一个类的内部,并不需要内部类引用外围类对象。为此,可以将内部类声明为
6.5 代理
假设有一个表示接口的
Class
对象(有可能只包含一个接口),它的确切类型在编译时无法知道。这确实有些难度。要想构造一个实现这些接口的类, 就需要使用newlnstance
方法或反射找出这个类的构造器。但是,不能实例化一个接口,需要在程序处于运行状态时定义一个新类。而代理机制则是一种更好的解决方案。代理类可以在运行时创建全新的类。这样的代理类能够实现指定的接口。尤其是,它具有下列方法:
- 指定接口所需要的全部方法。
Object
类中的全部方法,例如,toString
、equals
等。
要想创建一个代理对象, 需要使用Proxy
类的newProxylnstance
方法。这个方法有三个参数:
- 一个类加栽器(class loader)。作为 Java 安全模型的一部分, 对于系统类和从因特网上下载下来的类,可以使用不同的类加载器。有关类加载器的详细内容将在卷Ⅱ第9章中讨论。目前, 用 null 表示使用默认的类加载器。
- 一个 Class 对象数组, 每个元素都是需要实现的接口。
- 一个调用处理器。
/** 参考代码如下:**/
// 创建被代理对象的 调用处理器
Object value = ...;
InvocationHandler handler = new TraceHandler(value) ;
// 创建一个或多接口的代理
Class[] interfaces = new Class[] { Comparable.class};
Object proxy = Proxy.newProxylnstance(null , interfaces , handler);
/**
* 调用处理器的实现
**/
class TraceHandler implements InvocationHandler
{
private Object target;
public TraceHandler(Object t){
target = t;
}
public Object invoke(Object proxy, Method m, Object口 args) throws Throwable {
// print method name and parameters
// invoke actual method
return m.invoke(target , args);
}
}
代理类的特性
- 是在程序运行过程中创建的。一旦被创建,就变成了常规类。与虚拟机中的任何其他类没有什么区别。
- 所有的代理类都扩展于
Proxy
类。一个代理类只有一个实例域—调用处理器,它定义在Proxy
类中(PS:查看Proxy
的源码【JDK 1.8.0_121】可以发现存在实例域protected InvocationHandler h;
)。 - 所有的代理类都覆盖了
Object
类中的方法toString
、equals
和hashCode
。如同所有的代理方法一样,这些方法仅仅调用了调用处理器的invoke
。Object
类中的其他方法(如clone
和getClass
)没有被重新定义。 - 没有定义代理类的名字,Sun 虚拟机中的
Proxy类
将生成一个以字符串$Proxy
开头的类名。 - 对于特定的类加载器和预设的一组接口来说,只能有一个代理类。也就是说,如果使用同一个类加载器和接口数组调用两次
newProxylustance
方法的话,那么只能够得到同一个类的两个对象,也可以利用getProxyClass
方法获得这个类:Class proxyClass = Proxy.getProxyClass(null, interfaces)
- 代理类一定是
public
和final
。如果代理类实现的所有接口都是public
,代理类就不属于某个特定的包;否则,所有非公有的接口都必须属于同一个包,同时,代理类也属于这个包。 - 可以通过调用
Proxy
类中的isProxyClass
方法检测一个特定的Class
对象是否代表一个代理类。
第7章 异常、断言和日志
异常
Java 中的异常层次结构
所有的异常都是由
Throwable
继承而来,但在下一层立即分解为两个分支:Error
和Exception
。Java语言规范将派生于Error
类或RuntimeException
类的所有异常称为非受查(unchecked)异常,所有其他的异常称为受查(checked) 异常。编译器将核查是否为所有的受査异常提供了异常处理器。
Throwable | |||
---|---|---|---|
Error | Exception | ||
其他异常 (如 IOException) | Runtime Exception |
Error
类层次结构描述了 Java 运行时系统的内部错误和资源耗尽错误。应用程序不应该抛出这种类型的对象。Exception
RuntimeException
:由程序错误导致的异常属于 RuntimeException;
其派生异常类包含以下几种情况:- 错误的类型转换;
- 数组访问越界;
- 访问 null 指针;
- 而程序本身没有问题,但由于像 I/O 错误这类问题导致的异常属于其他异常。
非派生RuntimeException
的异常包含下面几种情况:- 试图在文件尾部后面读取数据;
- 试图打开一个不存在的文件;
- 试图根据给定的字符串查找 Class 对象, 而这个字符串表示的类并不存在;
如果在子类中覆盖了超类的一个方法, 子类方法中声明的受查异常不能比超类方法中声明的异常更通用(也就是说,子类方法中可以抛出更特定的异常,或者根本不抛出任何异常)。特别需要说明的是,如果超类方法没有抛出任何受查异常,子类也不能抛出任何受查异常。例如,如果覆盖
JComponent.paintComponent
方法,由于超类中这个方法没有抛出任何异常,所以,自定义的paintComponent
也不能抛出任何受查异常。
捕获异常
- 在重新抛出异常时,可以使用
throw new XXXXException("XXX 错误: " + e.getMessage());
的方式抛出异常,不过有一种更好的处理方式,并且将原始异常设置为新异常的“原因”:
try{
// access the database
}catch (SQLException e){
Throwable se = new ServletException ("database error");
se.ini tCause(e);
throw se;
}
强烈建议使用这种包装技术。这样可以让用户抛出子系统中的高级异常,而不会丢失原始异常的细节。并且在发生受检异常但不允许抛出它时,包装技术就十分有用,可以将它包装成一个运行时异常。当捕获到异常时, 就可以使用下面这条语句重新得到原始异常:
Throwable e = se.getCause();
- 强烈建议解搞合 try/catch 和 try/finally 语句块。 这样可以提高代码的清晰度。例如:
InputStrean in = . . .;
try {
try {
// code that might throw exceptions
} finally {
in.close();
}
} catch (IOException e) {
// show error message
}
内层的try语句块
只有一个职责,就是确保关闭输入流。外层的try语句块
也只有一个职责,就是确保报告出现的错误。这种设计方式不仅清楚,而且还具有一个功能,就是将会报告finally
子句中出现的错误。
- 带资源的 try 语句(try-with-resources)
try (Scanner in = new Scanner(new FileInputStream('/usr/share/dict/words"),"UTF-8");
PrintWriter out = new PrintWriter("out.txt")) {
while (in.hasNext())
System.out.println(in.next()) ;
}
- 使用异常机制的技巧
- 异常处理不能代替简单的测试。如果将
if (!s.empty()) s.pop()
替换为下列代码,运行时间将大大增加。try{ s.pop(); } catch (EmptyStackException e){ ... }
- 不要过分地细化异常。可以将整个任务装在一个
try
语句块中,当任意操作出现问题时,整个任务都可以取消。 - 利用异常层次结构。当一种异常可以转换成另一种更加适合的异常时不要犹豫。
- 不要压制异常。该处理就要处理,不要空
catch
,除非这个异常N年才触发一次。 - 在检测错误时,“苛刻” 要比放任更好。抛出更符合单前错误的异常,一般是首个。(早抛出)
- 不要羞于传递异常。让高层次的方法通知用户发生了错误,或者放弃不成功的命令更加适宜。(晚捕获)
断言
PS: 目前在实际开发时都没有用过断言。如果是需要“前置校验”,如非空、大于、小于等数据等,都可以使用校验框架(
javax.validation
)进行处理。而数据测试,则通过JUnit
编写测试用例。
- 关键字
assert
的两种形式:- assert 条件;【要想断言 x 是一个非负数值,
assert x >= 0;
】 - assert 条件:表达式;【将 x 的实际值传递给 AssertionError 对象
assert x >= 0 : x;
】
- assert 条件;【要想断言 x 是一个非负数值,
- 什么时候使用断言呢?
- 断言失败是致命的、不可恢复的错误。
- 断言检查只用于开发和测阶段(这种做法有时候被戏称为“在靠近海岸时穿上救生衣,但在海中央时就把救生衣抛掉吧”)
日志
- 记录日志API的优点
- 可以很容易地取消全部日志记录,或者仅仅取消某个级别的日志,而且打开和关闭这个操作也很容易。
- 可以很简单地禁止日志记录的输出, 因此,将这些日志代码留在程序中的开销很小。
- 日志记录可以被定向到不同的处理器,用于在控制台中显示,用于存储在文件中等。(日志管理器在 VM 启动过程中初始化, 这在 main 执行之前完成,如果main中再调用日志管理器初始化,则重新初始化。)
- 日志记录器和处理器都可以对记录进行过滤。过滤器可以根据过滤实现器制定的标准丢弃那些无用的记录项。
- 日志记录可以采用不同的方式格式化,例如,纯文本或 XML。
- 应用程序可以使用多个日志记录器,它们使用类似包名的这种具有层次结构的名字,例如,
com.mycompany.myapp
。 - 在默认情况下,日志系统的配置由配置文件控制。如果需要的话,应用程序可以替换这个配置(默认的日志配置记录了 INFO 或更高级别的所有记录)。
PS:目前开发中主要使用
slf4j
(simple log facade for java)作为日志记录器,代替之前使用的log4j
(log for java)。System.out.println
会线程阻塞的,别用。
区别:log4j
是真正实现日志功能的产品,像这样的产品有很多。而slf4j
是一个适配器,我们通过调用slf4j的日志方法统一打印我们的日志,而可以忽略其他日志的具体方法,这样,当我们的系统换了一个日志源后,不需要更改代码。