语言只是实现目标的工具,而不是目标本身。
基础语法
- 不要在程序中使用char,最好将字符串作为抽象数据处理
- 整数和布尔值之间不能相互转换
- 一定不要使用“==”运算符检测两个字符串是否相等,判断的是两个字符串是否存放在相同的位置。以及其他装箱类比如(Integer),而是使用
equals
。这个运算符只能确定两个字符串是否放置在同一个位置上。Integer只在-128~127有缓存, ==只会比较缓存是否一致,也就是指向的地址是否一致。所以(Integer)129 == (Integer)129是false。 - 有些时候需要由较短的字符串构建长字符串,每次拼接将会产生一个长字符串,比较浪费时间和空间。所以使用
StringBuilder
类就可以解决这个问题。
1 | StringBuilder builder = new StringBuilder(); |
- 用于printf的转换符
- 匿名数组
1 | // 既有数组初始化 |
- For each循环
1 | // for (variable : collection) statement |
- 数组拷贝
新数组拷贝旧数组中的各元素1
2import java.util.Arrays;
int[] copiedArray = Arrays.copyOf(Array, Array.length); java.util.Arrays
- 不规则数组
Java 实际上没有多维数组, 只有一维 数组。 多维数组被解释为数组的数组。
所以我们可以非常方便的作出一个将不同长度的数组作为元素的数组:
1 | int[][] triangleArray = new int[10][]; |
类和对象
注意不要编写返回引用可变对象的访问器方法
bad example:
1 | class Employee |
这样会造成如下结果:
1 | Employee Jobs = new Employee(); |
{% asset_img image-20210126221230569.png class_01 %}
如果需要返回一个可变对象的引用,应该首先对它进行克隆(clone)。
revised:
1 | class Employee |
一个类的方法可以访问这个类的任意对象的私有域
final实例域,构建对象时必须初始化这样的域。
- 如果该对象是不可更改类型,如String,在后面的操作中,不能对它进行修改。
- 如果该对象是可更改类型,如Date,在后面的操作中,该对象的引用不能指向其他对象,但是该对象可以修改。
static静态域和静态方法
- 如果将域定义为static,每个类中只能有一个这样的域。所有的实例共享一个静态域。
1 | class Employee { |
即使没有一个Employee实例,nextId
也存在,它属于类不属于实例。
- 静态常量(用的比较多)
1 | public class Math { |
如果没有static,需要每次创建一个实例才能调用该常量。如果只有static没有final,每个实例都能更改共有静态域。
-
静态方法
当有以下两种需求的时候可以使用静态方法:- 方法不需要访问对象状态, 其所需参数都只通过显式参数提供(例如: Math.pow )
- 方法只访问类的静态域
静态方法不能调用非静态实例域(隐式参数),因为实例域只有实例才有的域,而静态方法可以不创建实例就被调用,所以这种会引起错误的写法被禁止。
-
静态工厂方法
Reference
主要用途:摒弃用new和构造函数来创建实例的方法。
在外部调用的例子:1
2
3NumberFormat currencyFormat = NumberFormat.getCurrencyInstance();
NumberFormat percentFromat = Numberformat.getPercentInstance();
Integer number = Integer.valueOf("3");优点:
- 每个对象的创建都有相应的名称,可读性高
bad example: 全部用构造器创建,可读性低。(现在Date类都已经标记Depreciated)
1
Date date = new Date(121,11,25);
good example:
1
Date date = Date.valueOf("2021-12-25");
- 工厂方法实现单例
- 返回子类
1
2
3
4
5
6
7
8
9
10
11
12Class Person {
public static Person getPlayerInstance(){
return new Player();
}
public static Person getCookerInstance(){
return new Cooker();
}
}
Class Player extends Person{
}
Class Cooker extends Person{
}- 解决构造器重载无法实现的函数签名相同的情况
构造函数重载的缺陷:
1
2
3
4
5
6
7
8
9
10
11
12
13
14class Child{
int age = 10;
int weight = 30;
public Child(int age, int weight) {
this.age = age;
this.weight = weight;
}
public Child(int age) {
this.age = age;
}
public Child(int weight) { // ERROR: 函数签名重复
this.weight = weight;
}
}工厂方法实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class Child{
int age = 10;
int weight = 30;
public static Child newChild(int age, int weight) {
Child child = new Child();
child.weight = weight;
child.age = age;
return child;
}
public static Child newChildWithWeight(int weight) {
Child child = new Child();
child.weight = weight;
return child;
}
public static Child newChildWithAge(int age) {
Child child = new Child();
child.age = age;
return child;
}
}- 减少不必要的属性暴露
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class Player {
private static final int TYPE_RUNNER = 1;
private static final int TYPE_SWIMMER = 2;
private static final int TYPE_RACER = 3;
int type;
private Player(int type) { // 构造函数隐藏,避免外部调用
this.type = type;
}
public static Player newRunner() {
return new Player(TYPE_RUNNER);
}
public static Player newSwimmer() {
return new Player(TYPE_SWIMMER);
}
public static Player newRacer() {
return new Player(TYPE_RACER);
}
}- 测试的时候不用每次都要重新创建实例赋值
如果要写一连串的测试代码,如果需要测试的界面有多个,那么这一连串的代码可能还会被复制多次到项目的多个位置。用静态工厂方法代替手写构造器更为方便也使代码更加整洁。
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
26
27
28static class User{
String name ;
int age ;
String description;
public static User newTestInstance() {
User tester = new User();
tester.setName("隔壁老张");
tester.setAge(16);
tester.setDescription("我住隔壁我姓张!");
return tester;
}
}
// 调用的时候
/* AccountTester.java */
User tester = User.newTestInstance();
bindAccount(tester);
/* UiTester.java */
User tester = User.newTestInstance();
bindUI(tester);
// 使用构造器时的混乱
/* AccountTester.java */
User tester = new User();
tester.setName("隔壁老张");
tester.setAge(16);
tester.setDescription("我住隔壁我姓张!");
bindAccount(tester); - 每个对象的创建都有相应的名称,可读性高
-
main方法
: 每一个类可以有一个 main 方法。这是一个常用于对类进行单元测试的技巧。
this构造方法
:
- Java 要求,在构造方法中如果使用关键字 this 调用其他构造方法,则 this(参数列表) 语句必须出现在其他语句之前。
this
不能在静态代码块中使用:由于this
代表的是对象的引用,因此依赖于具体对象。
可见性修饰符和数据域封装
可见型修饰符 | 类内访问 | 包内访问 | 从子类访问 | 从不同包访问 |
---|---|---|---|---|
public | 可以 | 可以 | 可以 | 可以 |
protected | 可以 | 可以 | 可以 | 不可以 |
默认 | 可以 | 可以 | 不可以 | 不可以 |
private | 可以 | 不可以 | 不可以 | 不可以 |
子类可以覆盖父类的 protected 方法,并把该方法的可见性改成 public。但是子类不能降低父类方法的可见性,即不能把父类的 public 方法的可见性改成 protected。
类设计方法建议
- 一定要保证数据私有
- 一定要对数据初始化
- 不要在类中使用过多的基本类型,必要时新开类来代替一些实例域
- 不是所有的域都需要独立的域访问器和域更改器
- 将职责过多的类进行拆分
- 类名和方法名要提现他们的职责
- 优先使用不可变的类
方法参数
Java程序设计语言总是采用按值调用。 也就是说,方法得到的是所有参数值的一个拷贝,方法不能修改传递给它的任何参数变量的内容。
举例:
1 | public static void swap(Employee x, Employee y) { |
Java程序设计语言对对象采用的不是引用调用,对象是按值传递的。
- 一个方法不能修改一个基本数据类型的参数
- 一个方法可以改变一个对象参数的状态(用实例的方法比如setType())
- 一个方法不能让对象参数引用一个新的对象
继承与多态
重写
- 子类的方法返回值类型可以和父类不完全一致。如果返回值类型是基本数据类型或者
void
,则必须一致。如果返回值类型是引用类型,则要求返回值类型相同或者子类的方法的返回值类型是负累的烦法的返回值类型的子类。 - 重载和重写的区别:重载指在同一个类中定义多个方法,这些方法有相同的名称,但是方法签名不同;重写指在子类中定义一个方法,该方法与父类中的方法的签名相同。
抽象类
抽象类和常规类一样具有数据域、方法和构造方法,但是不能用new
操作符创建实例。
- 抽象类可包含抽象方法,也可以不包含抽象方法,抽象方法同样要用
abstract
修饰,只有方法签名而没有实现 - 非抽象类不能包含抽象。如果一个抽象父类的子类不能实现所有的抽象方法,则该子类也必须声明为抽象类
- 包含抽象方法的类必须声明为抽象类
多态
多态存在的三个必要条件
- 继承
- 重写
- 父类引用指向子类对象:Parent p = new Child();
能力: 同一个行为具有多个不同表现形式或形态。
以下例子输出A C
1 | public class demo { |
抽象类和接口的区别
- 抽象类的变量没有限制,接口只包含常量,即接口的所有变量必须是
public static final
。 - 抽象类包含构造方法,子类通过构造方法链调用构造方法,接口不包含构造方法。
- 抽象类的方法没有限制
- 一个类只能继承一个负累,但是可以实现多个接口。一个接口可以继承多个接口。
Object类
equals
用于检测一个对象是否等于另外一个对象。在Object类中,这两个方法将判断两个对象是否具有相同的引用,但是在类的重写中一般会改成值比较。
Java语言规范要求equals方法具有下面的特性:
- 自反性:
x.equals(x)
应该返回true - 对称性:当
y.equals(x)
返回true时,x.equals(y)
也应该返回true - 传递性:如果
x.equals(y)
返回true,y.equals(z)
返回true,x.equals(z)
也应该返回true - 一致性:如果x和y引用的对象没有发生变化,反复调用
x.equals(y)
应该返回同样的结果 - 对于任意非空引用x,
x.equals(null)
应该返回false
编写equals方法的建议:
- 显式参数命名为otherObject,稍后需要将它根据需要进行类型转换成other
- 检测this与otherObject是否引用同一个对象:
if (this == otherObject) return true
- 检测otherObject是否为null,如果为null,返回false
if (otherObject == null) return false
- 比较this与otherObject是属于一个类或者有一个共同子类
比如ArrayList中的equals比较:
1 | public boolean equals(Object o) { |
- 将otherObject转换成相应的类类型
- 使用
==
比较基本类型域,使用equals比较对象域
hashCode
-
Object类中默认的散列码为对象的存储地址。
-
如果重新定义equals方法,就必须重新定义hashCode,以便用户可以将对象插入到散列表中。
-
hashCode方法应该返回一个整型数值,并合理地组合实例域的散列码,以便能够让各个不同的对象产生的散列码更加均匀
-
hashCode方法应该返回一个整型数值(也可以是负数),并合理地组合实例域的散列码,以便能够让各个不同的对象产生的散列码更加均匀
-
如果组合和多个散列值时,可以简单的写成
Objects.hash(name, salary, hiredat)
-
Equals 与 hashCode 的定义必须一致: 如果 x.equals(y) 返回 true, 那么 x.hashCode( ) 就必须与 y.hashCode( ) 具有相同的值
为什么要有 hashCode?
我们以“ HashSet 如何检查重复”为例子来说明为什么要有 hashCode。
当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals() 方 法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。
为什么重写 equals 时必须重写 hashCode 方法?
如果两个对象相等,则 hashcode 一定也是相同的。两个对象相等,对两个对象分别调用 equals 方法都返回 true。但是,两个对象有相同的 hashcode 值,它们也不一定是相等的 。因此, equals 方法被覆盖过,则 hashCode 方法也必须被覆盖。还有就是,hashCode() 的默认行为是对堆上的对象地址产生独特值。如果没有重写 hashCode() ,则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
如果存在数组类型的域,可以使用静态的Arrays.hashCode方法计算散列码,这个散列码由数组元素的散列码组成。
toString
用于返回表示对象值的字符串。
Object类默认定义打印输出对象所属的类名和散列码。
试例:
1 | public String toStirng() { |
对象包装器与自动装箱
对象包装器类(wrapper): Integer, Long, Float, Double, Short, Byte, Character, Void, Boolean. (前6个类派生于公共的超类Number)
自动装箱(autoboxing):
1 | ArrayList<Integer> list = new ArrayList<>(); |
自动拆箱:
1 | int n = list.get(i); // 将自动转换成 |
如果在一个条件表达式中混合使用Integer和Double类型,Integer值就会拆箱提升为double,再装箱为Double。
装箱和拆箱是编译器认可的而不是虚拟机。编译器在生成类的字节码时,插入必要的方法调用。虚拟机只是执行这些字节码。
自动装箱规范要求boolean, byte, char <= 127, 介于-128~127之间的short和int被包装到固定的对象中。例如,如果在前面的例子中将a和b初始化为100, 对他们进行==比较的结果一定成立。
1 | Integer a = Integer.valueOf(128); |
参数数量可变的方法
1 | public static double max(double... values) { |
枚举类
简单的枚举类:
1 | public enum Size {SMALL, MEDIUM, LARGE, EXTRA_LARGE }; |
定义构造器、方法和域:
1 | enum Size { |
构造器只在构造枚举长江的时候被调用
所有的枚举类型都是Enum类的子类,因此继承了一些 Enum方法:
static Enum valueOf(Class enumClass, String name)
: 返回制定名字,给定类的枚举常量String toString()
: 返回枚举常量名int ordinal()
: 返回枚举亮在enum声明中的位置,位置从0开始计数int compareTo(E other)
: 如果枚举常量出现在other之前,则返回一个负值;如果this==other, 返回0;否则返回正值。
反射(reflective)
反射机制的功能:
- 在运行时分析类的能力
- 在运行时查看对象
- 实现通用的数组操作代码
- 利用Method对象
Class类
获得类对应的Class对象的方法:
- getClass
1 | Random generator = new Random(); |
- 静态方法Class.forName
1 | String className = "java.util.Random"; |
- 直接获取了心的类对象
1 | Class cl1 = Random.class; |
动态创建一个类的实例:
1 | e.getClass().newInstance(); |
检查类的结构
运行时反射分析对象域
反射方法不仅可以知晓实例类的域名,方法名和修饰符等,还可以在运行时查看对象域的值。如果f是一个Field类型的对象,obj是某个包含f域的类的对象,f.get(obj)将返回一个对象,其值为obj域的当前值。
1 | Employee harry = new Employee("Harry Kane", 35000, 10, 1, 1999); |
使用反射编写泛型数组代码
目标:实现一个通用方法能将任何数组复制并转换成Object[], 类似Arrays.copyOf()
错误示范:
1 | public static Object[] badCopyOf(Object[] a, int newLength) { |
此时生成的对象数组不能再转换为Employee[],否则会产生ClassCastException。将一个Employee[]临时地转换成Object[]数组,然后再把它转换回来是可以的,但一个从开始底层就是Object[]的数组却永远不能转换成Employee[]数组。
1 | Integer[] a = {1,2,3,4,5}; |
因此我们需要动态的创建与原数组类型相同的新数组
1 | Object newArray = Array.newInstance(componentType, newLength); |
为了获得型数组元素类型,就需要进行以下工作:
- 首先获得a数组的类对象
- 确认它是一个数组
- 使用Class类(只能定义表示数组的类对象)的getComponentType方法确定数组对应的类型
正确示例:
1 | public static Object goodCopyOf(Object a, int newLength) { |
调用方式:
1 | int[] a = {1,2,3,4,5}; |
传递类方法(类似函数指针)
- 通过反射得到方法
getMethod的函数签名:
1 | Method getMethod(String name, Class... parameterTypes) //匹配重载所以是多参列表 |
1 | Method sqrt = Math.class.getMethod("sqrt", double.class); |
- 通过Method类中的invoke方法调用包装在当前Method对象中的方法
invoke方法的函数签名:
1 | Object invoke(Object obj, Object... args) |
obj表示所要调用方法的实例,通过实例来调用隐式参数,如果是静态方法的话可以为null
args表示显示参数
代码示例:
1 | // 打印sqrt和sqr列表 |
继承的设计技巧
- 将公共操作和域放在超类
- 不要使用受保护的域
有些程序员认为,将大多数的实例域定义为 protected 是一个不错的主意,只有这样,子类才能够在需要的时候直接访问它们。然而,protected机制并不能够带来更好的保护,其原因主要有亮点。第一,子类集合是无限制的,任何一个人都能够由某个类派生一个子类,并编写代码以直接访问protected的实例域,从而破坏了封装性。第二,在Java中,在同一个包中的所有类都可以访问protected域,而不管它是否为这个类的子类。
- 使用继承实现"is-a"关系
- 除非所有继承的方法都有意义,否则不要使用继承
使用Holiday继承java.util.GregorianCalendar? No, 继承了的Holiday不是封闭的,GregorianCalendar的公有方法add,可以将假日转换成非假日,又因为重写不能缩小可见性,所以继承add对Holiday来说毫无意义。
- 在覆盖方法时,不要改变预期的行为
里式置换原则不仅应用于语法,而且也可以应用于行为,这更加重要。紧接上文,如果你仍要坚持Holiday继承GregorianCalendar,或许你可以想到重写add方法但是并不做真正的添加操作,或抛出异常,或什么也不做,或继续到下一个假日,然而这些都违反了置换原则。
- 使用多态,而非类型信息
Never code like this:
1 | if (x instanceof T1) |
- 不要过多地使用反射
接口、lambda表达式和内部类
接口
- 接口中所有方法自动地属于public
- 接口绝不能含有实例域,但可以包含常量(自动设为public static final)
- 可以使用instanceof检查一个对象是否实现了某个特定的接口
- 接口也可以继承(扩展)接口
- 在Java SE 8中,允许在接口中增加静态方法
- 可以为接口方法提供一个默认实现,这样实现类就可以挑选自己想要实现的方法实现,还可以解决“接口演化”的兼容问题
1 | interface Comparable<T> { |
默认方法冲突
情况一:实现两个接口,只要这两个接口的方法中有两个的函数签名相同并且其中至少有一个是默认方法,就必须解决二义性
1 | interface Named { |
情况二(类优先):继承一个超类,实现一个接口,超类包含了和接口默认方法同名的方法
1 | interface Named { |
此时调用Student实例的getName()将会只使用超类方法,接口对应的默认方法将会被忽略。
clone
默认的Object类clone方法是浅拷贝,只有实现Cloneable接口才能完成深拷贝。
lambda表达式
表达式例子
- 参数类型,参数
1 | (String first, String second) -> { |
- 泛型参数可推
1 | Comparator<String> comp = (first, second) -> { |
- 单参可推
1 | ActionListener listener = event -> |
- 无参
1 | () -> { for (int i=100; i >= 0; i--) System.out.println(i); } |
函数式接口
函数式接口
: 对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式
最好把lambda表达式看作是一个函数,而不是一个对象,另外lambda表达式可以传递到函数时接口。对lambda表达式所能做的也只是能转换为函数式接口。
如果想要用lambda表达式做某些处理,可以用一些特定的函数式接口比如java.util.function.Predicate
1 | public interface Predicate<T> { |
ArrayList类有一个removeIf方法,它的参数就是一个Predicate。这个接口专门用来传递lambda表达式,list.removeIf(e -> e == null)
。
方法引用
1 | Timer t = new Timer(1000, System.out::println); |
表达式System.out::println是一个方法引用(method reference),它等价于lambda表达式x -> System.out.println(x)
。
用操作符分隔方法名与对象或类名的几种情况:
object::instanceMethod
Class::staticMethod
Class::instanceMethod
super::instanceMethod
this::instanceMethod
构造器引用
构造器引用的方法名为new,如Person::new
,构造器的选择取决于上下文
1 | ArrayList<String> names = ...; |
可以用数组类型建立构造器引用:int[]::new
是一个构造器引用,它有一个参数即数组的长度,这等价于lambda表达式x->new int[x]
。
变量作用域
lambda表达式中访问外围方法或类中的变量
1 | public static void repeatMessage(String text, int delay) { |
lambda表达式的3个部分:
- 一个代码块
- 参数
- 自由变量的值,指非参数而且不在代码中定义的变量
lambda表达式中的自由变量text,因为会出现repeatMessage方法已经返回其本地变量被回收,但是其中的lambda函数还在每秒打印text,所以表示lambda表达式的数据结构必须存储自由变量的值。我们说它被lambda表达式捕获(captured),这样的lambda表达式可以称之为闭包(closure)。在lambda表达式中,自由变量只能引用值不会改变的变量。
下面几个案例是不合法的:
1 | public static void countDown(int start, int delay) { |
1 | public static void repeat(String text, int count) { |
1 | Path first = Paths.get("/usr/bin"); |
lambda表达式中捕获的变量必须实际上是最终变量(effectively final)
在一个lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数。
1 | public class Application() { |
处理lambda表达式
使用lambda表达式的重点是延迟执行(deferred execution),毕竟,如果想要立即执行代码,完全可以直接执行,而无需把它包装在一个lambda表达式中。
原因:
- 在一个单独的线程中运行代码
- 多次运行代码
- 在算法的适当位置于心代码
- 发生某种情况时执行代码(如点击了一个按钮或数据到达时)
- 只在必要时才运行代码
1 | // Comparator 静态方法创建比较器 |
内部类
内部类(inner class)是定义在另一个类中的类。
使用内部类的原因:
- 内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据
- 内部类可以对同一个包中的其他类隐藏起来
- 当想要定义一个回调函数且不想编写大量代码时,使用匿名(anonymous)内部类比较便捷
成员内部类
1 | class Circle { |
- 成员内部类可以无条件访问外部类的所有成员属性和成员方法(包括private成员和静态成员)。
- 显式访问外部类变量或方法:OuterClass.this.VariableName, OuterClass.this.MethodName
- 在外部类中如果要访问成员内部类的成员,必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问
- 成员内部类是依附外部类而存在的,也就是说,如果要创建成员内部类的对象,前提是必须存在一个外部类的对象。
1 | public class Test { |
- 内部类中声明的所有静态与都必须是final。我们希望一个静态域只有一个实例,不过对于每个外部对象,会分别有一个单独的内部类实例。如果这个域不是final,它可能就不是唯一的。
- 内部类不能有static方法。
- 内部类可以拥有private访问权限、protected访问权限、public访问权限及包访问权限。private修饰,则只能在外部类的内部访问;public修饰,则任何地方都能访问;protected修饰,则只能在同一个包下或者继承外部类的情况下访问;默认访问权限,则只能在同一个包下访问。
局部内部类
在一个方法中定义局部类
1 | class TalkingClock { |
局部类不能用public或private访问限制符进行修饰,它的作用域被限定在声明这个局部类的块中。
局部类的优势:对外部世界可以完全地隐藏起来。即使TalkingClock类中的其他代码也不能访问它。除start方法之外,没有任何方法知道TimePrinter类的存在。
为了能够让actionPerformed方法工作,由于参数beep会在start结束后回收,TimePrinter类在beep域释放之前将beep域用start方法的局部变量进行备份。
匿名内部类
将局部内部类的使用再深入一步。假如只创建这个类的一个对象,就不必命名了。这种类被称为匿名内部类(anonymous inner class)。
1 | public void start(int interval, boolean beep) { |
语法格式:
1 | new SuperType(construction parameters) { |
其中,SuperType可以是接口也,也可以是类(内部类扩展)。由于构造器的名字必须与类名相同,而匿名类没有类名,所以,匿名类没有构造器。构造器参数将被传递给超类的构造器。实现接口时便没有构造参数。
1 | // 扩展类的匿名类 |
Tips:
- 双括号初始化
1 | invite(new ArrayList<String>{{add("Kane"); add("Lampard")}}); |
外层括号建立了ArrayList的一个匿名子类,内层括号则是一个对象构造块。
- 匿名子类的equals方法要注意,考虑
getClass() != other.getClass()
- 生成日志或调试消息时,通常希望包含当前类的类名,如:
1 | System.err.println("Error in " + getClass()); |
不过,这对于静态方法不work。因为getClass调用的是this.getClass()
,而静态方法没有this。所以应该采用以下表达式:
1 | new Object(){}.getClass().getEnclosingClass() |
new Object(){}
会建立Object的一个匿名子类的一个匿名对象,getEnclosingClass
则得到其外围类,也就是包含这个静态方法的类。
静态内部类
使用内部类只是为了把一个类隐藏在另外一个类的内部,并不需要内部类引用外围类对象。为此,可以将内部类声明为static,以便取消产生的引用。
例子:遍历数组一次,同时计算出最小值和zuida值。
一般做法:在当前包见一个Pair类,包含两个内部域,再写个方法遍历然后返回值为Pair。在大型项目中,Pair这样大众化的名字很可能被其他程序猿所定义,有可能会产生名字冲突。所以可以将Pair定义为内部公有类来访问。
静态内部类做法:将Pair作为内部公有类定义在ArrayAlg的内部,然后通过ArrayAlg.Pair访问它ArrayAlg.Pair p = ArrayAlg.minmax(d);
。由于Pair对象中不需要引用任何其他的对象,为此,可以将这个内部类声明为static。
代理
利用代理可以在运行时创建一个实现了一组给定接口的新类。这种功能只有在编译时无法确定需要实现哪个接口时才有必要使用。这种功能只有在编译时无法确定需要实现哪个接口时才有必要使用。
多线程
创建线程的方法
继承Thread类
1 | public class MyThread extends Thread { |
调用:
1 | MyThread t = new MyThread(); |
实现Runnable接口
1 | public class MyThread implements Runnable { |
调用:
1 | MyThread mThread = new MyThread(); |
实现Callable接口
JDK5.0 新增
-
创建实现Callable的实现累
-
实现call方法,将此线程需要中的操作声明在call()中
-
创建Callable接口实现类的对象
-
将此Callable接口实现类的对象作为和传递到FutureTask构造器中,创建FutureTask的对象
-
将FutureTask的对象(FutureTask实现了Runnable和Callable接口)作为参数传递到Thread的构造器中,并调用start()
-
通过FutureTask的get()方法来获取call()方法的返回值
1 | // 调用 |
使用线程池
1 | SumThread sumThread = new SumThread(); |
四种方式的比较
- Java只有单继承,如果使用继承的方式就只能继承Thread
- 继承方式的共享数据必须使用静态属性,而实现类由于可以重复使用单个实例所以天然共享属性值
- Thread类其实本身也实现了Runnable接口,两种方式都需要重写run()
- Callable接口中的call方法可以抛出异常,可以有返回值,支持泛型
- 线程池可以减少创建、销毁新线程的时间;降低资源消耗;便于线程管理(corePoolSize(核心池大小),maximumPoolSize(最大线程数),keepAliveTime(线程没有任务时最多保持多长时间后会终止))
1 | ExecutorService service = Executors.newFixedThreadPool(5); |
同步的方式
-
同步代码块:
继承Thread类的同步方式
1
2
3synchronized(this.class) {
.....
}实现Runnable类的同步方式
1
2
3synchronized(this) {
...
}多个线程必须要共用同一把锁(同步监视器)
-
同步方法synchronize修饰方法
同步方法不需要显示声明同步监视器。
非静态的同步方法,同步监视器是this
静态的同步方法,同步监视器是当前类本身(class)
线程安全式单例
-
懒汉式单例
1
2
3
4
5
6
7
8
9
10public class Bank{
private static Bank instance = null;
public static Bank getInstance() {
if (instance == null) {
instance = new Bank();
}
return instance;
}
} -
饿汉式单例
1 | public class Bank{ |
- 线程安全式懒汉式单例
1 | public class Bank{ |
线程的生命周期
- New 新建
- Runnable 可运行
- Blocked 被阻塞
- Waiting 等待
- Timed waiting 计时等待
- Terminated 被终止
线程的优先级
MAX_PRIORITY: 10
MIN_PRIORITY: 1
NORM_PRIORITY: 5
线程通信
wait()
: 执行此方法后进入阻塞状态并释放同步监视器notify()
: 执行此方法后就会唤醒一个wait中的线程,优先换新优先级高的notifyAll()
: 一旦执行此方法,就会唤醒所有被wait的线程
说明:
wait()
,notify()
,notifyAll()
三个方法必须使用在同步代码块或同步方法中。wait()
,notify()
,notifyAll()
三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则引发IllegalMonitorStateException
异常。wait()
,notify()
,notifyAll()
定义在java.lang.Object中。原因在于任何类都可以充当同步监视器,所以其子类都应该继承该方法。
sleep() 和 wait() 的异同
相同点:一旦执行方法,都可使得当前的线程进入阻塞状态。
不同点:
- 两个方法声明的位置不同:Thread类中声明sleep(), Object类中声明wait()
- 调用的要求不同:sleep() 可以在任何需要的场景下调用。wait()必须使用在同步代码块或者同步方法中
- sleep方法不会释放同步监视器,而wait会释放同步监视器,wait()阻塞后需要被notify()唤醒
Thread类的方法
- getPriority(): 返回线程优先值
- setPriority(int newPriority): 改变线程的优先级
高优先级的线程会抢占地优先级线程cpu的执行权,但只是从概率上讲,不能保证高优先级的线程一定在低优先级线程之前执行。
ReentrantLock
可重入性
可重入性是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁。如果是不可重入锁,那么在将要第二次获得锁时,自己也会被阻塞。
可打断性
lock.lock
:不可打断锁
lock.lockInterruptedly
: 可以被其他线程打断
锁超时
尝试活得锁,等待超过一定时间就放弃获得锁
1 | try {if (!lock.tryLock(1, TimeUnit.SECONDS) /* 尝试等待1秒,如果获得到锁返回真,否则假 */) {} |
容器
别用Stack!
详情
stack的推荐写法:
1 | Deque<Integer> stack = new ArrayDeque<>(); |
不推荐的写法:
1 | Stack<Integer> stack = new Stack<>(); |
原因:
Java中的Stack继承了Vector,从而导致了Stack除了自身该有的方法之外还包括了很多父类Vector的公共方法,比如动态数组的add
1 | stack.add(1, 666); |
这破坏了对于栈的封装。从设计原理上来说Stack和Vector之间的关系,不应该是继承关系,而应该是组合关系(composition),也就是"has-a"的关系。
那为什么Java官方推荐使用Deque
接口呢?
接口最大的意义在于解耦了Stack和其底层结构。底层开发人员可以随意维护自己的LinkedList类或者ArrayDeque类,只要他们满足Queue接口规定的规范;开发者可以选择合适的数据结构来定义Queue。
Deque的问题
Deque是一个双向列表,也就是说可以在两端都可以做插入和删除的操作,这也同样违背了栈只能在一端做插入和删除的概念。如何解决?只能自己再在这个基础之上封装一层。
1 | public interface Stack<T> { |
为什么是ArrayDeque?
动态数组扩容操作,链表不涉及扩容理论上时间复杂度为。虽然如此,可实际上,当数据量达到一定成都的时候,链表的性能是远远低于动态数组的。这是因为,每添加一个元素,都需要重新创建一个Node类的对象,也就是都需要进行一次new的内存操作。而对内存的操作,是非常慢的。在实践中,尤其面对大规模数据的时候,不应该使用链表
。