Java笔记_8
接口
接口用来描述类应该做什么, 不指定具体如何做, 一个类可以实现多个接口
- 接口可以定义常量, 但是绝不能有实例字段
Java 8
之前, 接口中的方法都是抽象方法
- 定义接口的方法不必指定为
public
, 因为接口方法自动为public
- 接口中的字段都是
public static final
的, 也不需要手动指定 - 但是实现接口时, 必须明确写
public
, 否则编译器会默认认为这个方法的访问属性是包可访问, 就会报错
1 | class E implements Comparable<E> { |
Comparable
接口文档建议compareTo
方法与equals
方法兼容, 即x.equals(y)
时,x.compareTo(y) == 0
- 大部分都是兼容的, 除了
BigDecimal
x = new BigDecimal("1.0")
和y = new BigDecimal("1.00");
因为精度不同, 所以x.compareTo(y) == 0
但是x.equals(y) == false;
- 理论上应该不返回
0
, 但是不知道谁大谁小
- 语言标准规定
x.compareTo(y) == -y.compareTo(x)
, 同样如果前者抛出异常, 后者也必须抛出异常 - 如果
M
继承自E
,E
实现了Comparable<E>
, 而没有实现Comparable<M>
- 如果要在
M
中重写, 就要做好准备比较M
和E
, 不能简单的将E
转换为M
- 比如
x
是M
,y
是E
, 调用y.compareTo(x)
不会抛出异常, 调用x.compareTo(y)
就会抛出一个ClassCastException
- 可以在每个
compareTo
之前都进行检测:if (getClass != oth.getClass()) throw new ClassCastException
接口属性
- 接口不是类, 不能使用
new Comparable()
- 但是可以使用接口变量, 必须引用一个实现了这个接口的对象
Comparable x = new E()
- 可以使用
instanceof
检查某个对象是否实现了一个接口 - 可以使用
extends
扩展接口, 一个类只能有一个超类, 但是可以实现多个接口
- 记录和枚举类不能扩展其他类, 因为他们隐式扩展了
Record
和Enum
类, 但是他们可以实现接口 - 接口也可以是密封的
sealed
, 直接子类型, 必须声明在permits
中, 或者在一个文件中
默认方法
-
可以为接口提供一个
default
方法, 表示方法的默认实现1
2
3
4
5public interface Comparable<T> {
default int compareTo(T oth) {
return 0;
}
} -
大部分情况没什么用, 因为每个实现的接口都会覆盖这个方法
-
不过有时候也能有用, 比如
Iterator
接口, 声明了一个remove()
1
2
3
4
5public interface Iterator<E> {
default void remove() {
throw new UnsupportOperationException("remove");
}
} -
默认方法也可以调用其他方法
-
默认方法可以实现接口演化, 实现代码兼容. 比如以前有一个类
class B implements Collection
, 后来Java
版本更新,Collection
接口中添加了新的方法, 如果不用default
修饰新的方法就会导致原来的类B
无法编译 -
如果在一个接口中定义了一个方法, 在超类或者另一个接口中定义了同样的方法,
Java
有自己的规则:- 超类优先: 如果超类定义了一个具体方法, 同名且有相同参数类型的默认方法会被忽略
- 接口冲突: 如果一个接口提供了一个默认方法, 另一个接口提供了一个同名并且参数类型相同的方法, 不论是否为默认方法, 就需要覆盖这个方法来解决冲突
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16interface Person {
default String getName() {return "";}
}
interface Name {
default String getName() {return getClass().getName() + "_" + hashCode();}
}
// 如果有一个类同时实现了这两个接口
class Student implements Person, Name {}
// 编译器会报错, 需要在Student类中提供一个getName(), 可以选择两个冲突方法中的一个
class Student implements Person, Name {
public String getName() {
return Person.super.getName();
}
}
// 就算如果Name类中没有定义默认的getName()方法, 编译器还是会报错, 要求程序员解决二义性问题
// 如果两个类都没有提供默认的getName()方法, 就不会有冲突- 另一种情况是类扩展了超类, 同时实现了一个接口, 超类和接口继承了相同的方法
1
2
3
4class Student extends Person implements Name {}
// 这种情况下只会考虑超类的方法, 接口所有的默认方法都会被忽略
// 类优先的规则可以确保和Java 7的兼容性
// 如果为一个接口添加默认方法, 对于有默认方法之前的版本代码不会有影响
- 绝对不能为
Object
某个方法定义默认方法, 比如toString(), equals()
- 因为类优先的规则, 这样的方法绝对无法超越
Object.toString()
或Objects.equals()
Comparator
接口
-
如果调用
Arrays.sort()
对字符串进行排序的话, 会按照字典序 -
现在如果想要按照字符串的长度进行排序, 就不能修改
String.compareTo()
-
可以使用
Arrays.sort()
方法的第二个版本, 接受一个数组和一个比较器作为参数, 比较器是实现了Comparator
接口的类的实例1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public interface Comparator<T> {
int compare(T first, T second);
}
// 比如需要按照长度比较字符串, 可以以如下方法实现
class LengthComparator implements Comparator<String> {
public int compare(String first, String second) {
return first.length() - second.length();
}
}
// 完成比较需要建立一个实例
var comp = new LengthComparator();
if (comp.compare(words[i], words[j]) > 0) {}
// 如果需要对一个数组进行排序, 可以调用 Arrays.sort()
String[] f = {};
Arrays.sort(f, new LengthComparator); -
静态
comparing
方法接受一个键提取器函数, 将类型T
映射为一个可比较的类型, 比如String
- 要比较的对象引用这个函数, 然后对返回的键完成比较, 比如假设有一个
Person
数组, 可以按照名字进行排序 Arrays.sort(people, Comparator.comparing(Person::getName));
- 可以把比较器和
thenComparing()
串起来, 处理比较相同的结果 Arrays.sort(people, Comparator.comparing(Person::getLastName).thenComparing(Person::getFirstName));
- 可以给
comparing
和thenComparing
提取的键指定一个比较器, 完成对人名长度的排序 Arrays.sort(people, Comparator.comparing(Person::getName, (s, t) -> Integer.compare(s.length(), t.length())));
Arrays.sort(people, Comparator.comparingInt(p -> p.getName().length()));
- 要比较的对象引用这个函数, 然后对返回的键完成比较, 比如假设有一个
-
如果键函数可能返回
null
, 就需要用到nullsFirst
和nullsLast
适配器, 可以修改比较器, 遇到null
的时候不会抛出异常, 而是标记当前值小于或大于正常值Comparator.comparing(Person::getMiddleName(), Comparator.nullsFirst(...));
nullsFirst
方法需要一个比较器, 就是两个字符串的比较器naturalOrder
方法可以为任何实现了Comparable
的类建立一个比较器Arrays.sort(people, comparing(Person::getMiddleName, nullsFirst(naturalOrder()));
- 静态
reverseOrder
方法可以提供逆序, 等同于naturalOrder().reversed()
Cloneable
接口
clone
是Object
的protected
方法, 不能直接调用这个方法, 子类只能调用受保护的clone()
来克隆他自己的对象, 如果其中包含了一些其他对象, 就没有办法clone
了- 默认的克隆操作是一个浅拷贝, 没有克隆对象中引用的其他对象
- 如果原对象和浅克隆对象共享的子对象是不可变的, 那么这种共享就是安全的, 比如
String
- 或者在对象生命周期中, 子对象一直保持不变,没有更改器方法改变它, 也没有方法生成他的引用, 这种情况下就是安全的
- 但是大多数情况下, 子对象都是可变的, 必须重新定义
clone
方法, 需要确定- 默认的
clone
方法能满足要求 - 可以在可变子对象上调用
clone
弥补默认的clone
- 不能使用
clone
- 如果指定第一项或者第二项, 需要实现
Cloneable
接口, 重新定义clone
方法, 同时指定public
- 默认的
Cloneable
接口是Java
中少数的标记接口, 记号接口- 用途是确保一个类实现一个特定的方法或一组方法, 标记接口不包含任何方法, 唯一的作用就是允许在类型查询中使用
instabceof
- 自己写代码不要使用标记接口
- 即使默认的
clone()
可以满足要求, 还是需要实现Clonebale
接口, 将clone
重新定义为public
, 调用super.clone()
1
2
3
4
5
6class E implements Cloneable {
public E clone() throws CloneNotSupportException {
E cloned = (E) super.clone();
cloned.Day = (Date) Day.clone();
}
}
- 需要注意子类的克隆, 一旦为
E
定义了clone()
, 别人就可以使用他克隆子类M
- 所以最好避免使用
clone()
, 使用另一个方法达到同样的目的
lambda
表达式
lambda
表达式是一个可传递的代码块, 可以在以后执行一次或多次
- 以上面的排序为例,
first.length() - secode.length()
其中first, second
都是字符串1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23// lambda表达式写法
(String first, String second) -> {
if (first.length() < second.length()) return -1;
if (first.length() > second.length()) return 1;
else return 0;
}
// 即使lambda表达式没有参数, 仍然需要提供空括号
() -> {
for (int i = 100; i >= 0; i --) {
System.out.println(i);
}
}
// 如果可以推导出lambda表达式的类型, 就可以忽略类型
Comparator<String> comp = (first, second) -> {
first.length() - second.length();
}
// 如果只有一个参数, 并且参数类型还可以推导出来, 那么还可以省略小括号
ActionListener listener = event -> {
System.out.println("1111" + Instant.ofEpochMilli(event.getWhen()));
}
// 不需要指定lambda表达式的返回类型, 因为返回类型一定会根据上下文推导得到
// 可以使用var指示一个推导的类型, 不常用, 一般为了关联注解
(var first, var second) -> first.length() - second.length();
- 如果一个
lambda
表达式只有部分分支有返回值, 是不合法的
函数式接口
- 对于只有一个抽象方法的接口, 需要这种接口的对象时, 就可以提供一个
lambda
表达式, 称为函数式接口1
2
3
4
5
6
7// 比如Arrays.sort()第二个参数需要一个Comparator实例
// 只有一个抽象方法的接口, 就可以改成lambda表达式
Arrays.sort(words, (first, second) -> first.length() - second.length());
// 可以把lambda表达式看作一个函数, 而不是一个对象, 同时lambda表达式可以传递到函数式接口
// 不能把lambda表达式赋值给类型为Object的变量, Object不是一个函数式接口
// ArrayList通过lambda表达式删除一个数组列表中所有的null
list.removeIf(e -> e == null);
方法引用
1 | var timer = new Timer(1000, event -> System.out.println(event)); |
- 使用
::
操作符分割方法名和对象或者类名, 有三种情况:object::instanceMethod
- 方法引用等价于一个
lambda
表达式, 参数传递到方法, 对于System.out::println
, 对象是System.out
, 方法等价于x -> System.out.println(x)
- 方法引用等价于一个
Class::instanceMethod
- 第一个参数会成为隐式参数, 比如
String::compareToIgnoreCase
等同于(x, y) -> x.compareToIgnoreCase(y)
- 第一个参数会成为隐式参数, 比如
Class::staticMethod
- 所有参数都传递到静态方法,
Math::pow
等价于(x, y) -> Math.pow(x, y)
- 所有参数都传递到静态方法,
- 只有当
lambda
表达式的体只调用一个方法而不做其他操作的时候, 才能将其转换为方法引用 s -> s.length() == 0
里面只有一个方法调用, 但是还有一个比较, 所以不能使用方法引用- 方法引用不会独立存在, 总是会转换为函数式接口的实例
- 如果要删除数组中所有为空的值, 可以使用
list.removeIf(Objects::isNull)
- 包含对象的方法引用与等价的
lambda
表达式的区别, 比如separator::equals
, 如果separator
为null
, 构造separator::equals
时就会立即抛出NullPointerException
异常,lambda
表达式x -> separator.equals(x)
只会在调用时才会抛出NPE
- 可以在方法引用中使用
this
参数, 比如this::equals
等同于x -> this.equals(x)
- 同样使用
super
也是可以的
构造器引用
和方法引用类似, 只不过方法名为
new
, 比如Person::new
是Person
的构造器引用, 使用哪一个构造器取决于上下文
1 | ArrayList<String> names = ...; |
int[]::new
有一个数组的长度作为参数, 是一个构造器引用Java
中无法构造泛型类型T
的数组, 数组构造器可以克服这个限制
1 | Object[] people = stream.toArray(); |
变量作用域
- 一个
lambda
表达式有三个部分- 一个代码块
- 参数
- 自由变量: 指非参数, 并且不在代码中定义的变量
lambda
会将自由变量的值复制到lambda
表达式的数据结构实例对象中
lambda
表达式中只能引用不会改变的值, 比如引用int
就是不合法的- 如果在
lambda
表达式中更改变量, 并发执行多个动作的时候就不安全 - 就算
lambda
表达式内部没有修改变量, 但是这个变量也有可能在外部改变, 这也是不合法的 lambda
表达式捕获的变量必须是事实最终变量, 指的是这个变量初始化以后不会赋新值, 比如String
lambda
表达式中不能声明和局部变量同名的参数或变量
- 在
lambda
表达式中使用this
参数的时候, 是指创建这个lambda
表达式的方法的this
参数1
2
3
4
5
6
7
8public class A {
public void init() {
B b = event -> {
System.out.println(this.toString());
}
}
}
// 这里的this.toString()调用的是A对象的toString(), 而不是B实例方法
处理lambda
表达式
lambda
表达式的重点是延迟执行
- 延迟执行的原因
- 单线程
- 多次运行代码
- 在算法的适当位置运行, 比如排序的比较操作
- 发生某种情况的时候运行, 比如点击按钮, 数据到达
- 只有必要时才运行代码
1 | repeat(10, () -> System.out.println("1")); |
- 如果设计自己的函数式接口, 里面只有一个抽象方法, 可以使用
@FunctionalInterface
注解来标记接口- 这样如果添加了一个新的抽象方法, 可以检查出来并报错
- 同时
javadoc
会标记这个接口是一个函数式接口 - 不是必须使用这个注解
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Sangs Blog!