Java核心技术卷一 4. java接口lambda内部类和代理
Posted lovezyu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java核心技术卷一 4. java接口lambda内部类和代理相关的知识,希望对你有一定的参考价值。
接口
接口概念
接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。
如果类遵从某个特定接口,那么久履行这项服务。
public interface Comparable<T>{
int compareTo(T other);
}
任何实现 Comparable 接口的类都需要包含 compareTo 方法,并且这个方法的参数必须是一个 T 对象,返回一个整形数值。
接口的特点:
- 接口中所有方法自动地属于 public,所以接口的方法不需要提供关键字 public 。
- 接口中可以定义常量。更多请看接口的特性
接口不能提供的功能:
- 不能含有实例域
- 不能在接口中实现方法(java 8 之前)
接口与抽象类的区别:
- 可以将接口看成是没有实例域的抽象类,但还是有一定区别。
让类实现一个接口的步骤:
- 将类声明为实现给定的接口。
- 对接口中的所有方法进行定义。
class Employee implements Comparable<Employee> {
public int compareTo(Employee other){
return Double.compare(salary, other.salary);
}
...
}
实现类中的特点:
- 实现方法的方法声明为 public ,因为接口中的方法都自动地是 public。
- 为泛型 Comparable 接口提供一个类型参数,就可以不使用 Object 类型,使得程序省略了强制装换的步骤。
当使用 Array.sort() 方法时,必须实现 Comparable 接口方法,并且元素之间必须是可比较的,不然会报异常:
public class ArrayGood {
public static void main(String[] args) {
int[] a = Arrays.copyOf(new int[2], 100);
System.out.println(a.length);//100
Employee[] employees = new Employee[10];
employees = Arrays.copyOf(employees, 100);
int[] aint = {5, 3, 6, 14, 9, 7, 22, 10};
System.out.println(Arrays.toString(aint));
Arrays.sort(aint);
System.out.println(Arrays.toString(aint));
Employee[] employees1 = {new Employee("n")
, new Employee("h")
, new Employee("e")
, new Employee("n")
, new Employee("a")
, new Employee("r")
, new Employee("n")
, new Employee("i")
, new Employee("m")};
Arrays.sort(employees1);//java.lang.ClassCastException: Employee cannot be cast to java.lang.Comparable
}
}
public class Employee {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
Employee(String name){
this.name = name;
}
}
实现接口并实现方法后,Employee 数组调用了排序 sort() 方法,实现了排序:
public class ArrayGood {
public static void main(String[] args) {
int[] a = Arrays.copyOf(new int[2], 100);
System.out.println(a.length);//100
Employee[] employees = new Employee[10];
employees = Arrays.copyOf(employees, 100);
int[] aint = {5, 3, 6, 14, 9, 7, 22, 10};
System.out.println(Arrays.toString(aint));
Arrays.sort(aint);
System.out.println(Arrays.toString(aint));
Employee[] employees1 = {new Employee("n")
, new Employee("h")
, new Employee("e")
, new Employee("n")
, new Employee("a")
, new Employee("r")
, new Employee("n")
, new Employee("i")
, new Employee("m")};
Arrays.sort(employees1);
for (Employee emp:
employees1 ) {
System.out.print(emp.getName() + " ");
}
//a e h i m n n n r
}
}
public class Employee implements Comparable<Employee>{
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
Employee(String name){
this.name = name;
}
@Override
public int compareTo(Employee o) {
return name.compareTo(o.name);
//return Double.compare(double x, double y);
//return Integer.compare(int x, int y);
}
}
所以,要让一个类使用排序服务必须让它实现 compareTo 方法。这是理所当然的,因为要向 sort 方法提供对象的比较方法。
疑问:为什么不能在 Employee 类直接提供一个 compareTo 方法,而必须实现 Comparable 接口呢?
解答:原因在于 Java 程序设计语言是一种强类型语言。在调用方法的时候,编译器将会检查这个方法是否存在。调用 compareTo 方法时,sort 传入的 Object 对象会被强制转换为 Comparable 类型,因为只有一个 Comparable 对象才确保有 compareTo 方法。又因为存在这个强制转换,所以类必须还实现 Comparable 接口,这样才可以将 Object 引用的参数转化为一个 Comparable。
反对称规则:如果子类之间的比较含义不一样,那就属于不同类对象的非法比较。每个 compareTo 方法都应该在开始时进行下列检测
if (getClass() != other.getClass()) throw new ClassCastException();
接口的特性
接口不是类,不能使用new实例化
可以声明接口的变量
接口变量必须引用实现了接口的类对象,可以使用instance检查一个对象是否实现了某个特定的接口
java x = new Comparable(...);//ERROR Comparable x;//OK x = new Employee(...)//Employee实现了接口 if(anObject instanceof Comparable) {...}
- 如果类不实习接口的方法,那么这个类就要定义为抽象类
```java
public interface Named {
String getName();
}
abstract class Student implements Named{
}
```
- 接口可以被扩展,并且能用 extends 扩展多个接口
```
public interface Moveable{
void move(double x, double y);
}
public interface Powered extends Moveable, Comparable
- 接口不能包含实例域或静态方法,但可以包含常量,接口中的域自动设为
public static final
public interface Powered extends Moveable{ double milesPerGallon(); double SPEED_LIMIT = 95; }
- 接口只能继承一个超类,却可以实现多个接口
class Employee implements Cloneable, Comparable{ ... }
接口与抽象类
疑问:为什么引用接口概念,为什么不将 Comparable 直接设计为抽象类。
解答:如果使用抽象类表示通用属性存在一个问题,每个类只能扩展一个类。每个类却可以实现多个接口。
没有多重继承:许多设计语言允许一个类有多个超类,如 C++。而 Java 没有多继承是因为它会让语言本身变得非常复杂,降低效率。
静态方法
Java SE 8 中,允许在接口中增加静态方法。这有违接口作为抽象规范的初衷。
目前的方法(2018)都是将静态方法放在伴随类中。标准库中的接口和工具类,可能只包含一些工厂方法。
如 Path 接口定义了 Paths 类中的工厂方法,这样一来,Paths 类就不再是必要的了。
默认方法
Java SE 8 中可以为接口方法提供一个默认实现。必须用 default 修饰符标记:
public interface Comparable<T> {
default int compareTo(T oter){
return 0;
}
}
一般情况没有用处,因为方法会被覆盖。
但有时一个接口定义了大量的方法,我们又不需要实现这么多方法,只关心其中一两个方法。在 Java SE 8 中,可以把所有方法声明为默认方法,这些默认方法声明也不做。
public interface MouseListener{
default void moseClicked(MouseEvent event){}
default void mosePressed(MouseEvent event){}
default void moseReleased(MouseEvent event){}
default void moseEntered(MouseEvent event){}
default void moseExited(MouseEvent event){}
}
默认方法可以调用任何其他方法:
public interface Collection{
int size();
default boolean isEmpty(){
retrun size()==0;
}
...
}
public class Test implements Collection{
public static void main(String[] args) {
Test test = new Test();
System.out.println(test.size());//0
System.out.println(test.isEmpty());//true
}
@Override
public int size() {
return 0;
}
}
这样实现 Collection 的程序员就不用操心实现isEmpty
方法了。
解决默认方法冲突
如果接口中定义了默认方法,然后又在超类或另一个接口中定义了同样的方法,规则如下:
- 超类优先。如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略。
- 接口冲突。如果一个超接口提供了一个默认方法,另一个接口提供了自个同名而且参数类型相同的方法,必须覆盖这个方法来解决冲突。
来看看接口冲突的场景:
interface Named{
default String getName() {
return getClass().getName() + "_" + hashCode();
}
...
}
interface Person {
default String getName() {
return getClass().getName() + "_" + hashCode();
}
...
}
class Student implements Person, Named {
public String getName() {
return Person.super.getName();
}
}
当 Student 继承两个接口时,Java 编译器会报告一个错误,让程序员重写有冲突的方法解决二义性。我们使用接口类型.super.接口方法
的方法,在两个接口中选择一个方法,解决二义性。
假设 Named 接口没有为 getName 提供默认实现:
interface Named{
String getName();
}
如果这两个接口有一个提供了实现,那么编译器就会报告错误,让程序员解决二义性。
如果两个接口没有提供默认实现,那么编译器不会报错,程序员实现这个方法即可。不实现他们的方法,则类定义为抽象类。
类优先规则
如果类继承的类和继承的接口有相同的方法,那么接口的默认方法都会被忽略。
接口示例
接口与回调
回调:常见的程序设计模式,可以指出某个特定事件发生时应该采取的动作。
下面程序给出了定时器和监听器的操作行为。定时器启动以后,程序弹出一个消息对话框,并等待用户点击 OK 按钮来终止程序的执行。在程序等待用户操作的同时,每隔 10 秒显示一次当前的时间。
public class TimerTest {
public static void main(String[] args) {
ActionListener listener = new TimePrinter();
Timer t = new Timer(10000, listener);
t.start();
JOptionPane.showMessageDialog(null, "Quit program?");
t.stop();
}
static class TimePrinter implements ActionListener{
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("At the tone, the time is" + new Date());
}
}
}
Comparator 接口
Arrays.sort 的用比较器作为参数,比较器实现了 Comparator 接口的类的实例:
public interface Comparator<T>{
int compare(T first, T second);
}
如果要比较字符串:
class LengthComparator implements Comparator<String>{
public int compare(String first, String second){
return first.compareTo(second);
//return first.length() - second.length();
}
}
具体完成比较时,需要建立一个实例:
LengthComparator comp = new LengthComparator();
if (comp.compare(words[i], words[j]) > 0)) ...
与words[i].compareTo(words[j])
比较,compare 方法要在比较器对象上调用,而不是在字符串本身上调用。
要对一个数组排序,需要为 Arrays.sort 方法传入一个 LengthComparator 对象:
String[] friends = {"Peter", "Paul", "Mary"};
Arrays.sort(friends, new LengthComparator());
对象克隆
讨论 Cloneable 接口,它指示一个类提供一个安全的 clone 方法。克隆不太常见,细节技术性强,不做深入讨论。
首先,回忆一个包含对象引用的变量建立副本时会发生什么。原变量和副本都是同一个对象的引用:
Employee original = new Employee("John Public", 50000);
Employee copy = original;
copy.raiseSalary(10);//original 也会改变
说明,引用相同的任何一个变量改变都会影响另一个变量。
想要让 copy 的初始状态与 original 相同,但之后他们各自又会有不同的状态,这种情况就要使用 clone 方法:
Employee copy = original.clone();
copy.raiseSalary(10);//original 没有改变
这种拷贝基于 Object 类的 clone 方法属于浅拷贝,如果拷贝的对象的域有子对象,他们之间还是会有联系,改变子对象时,被浅拷贝的对象的子对象也会改变。
通常子对象都是可变的,必须重新定义 clone 方法来建立一个深拷贝,同时克隆所有子对象。
需要确定:
- 默认的 clone 方法是否满足要求;
- 是否可以在可变的子对象上调用 clone 来修补默认的 clone 方法;
- 是否不该使用 clone。
选择 1 或 2,类必须:
- 实现 Cloneable 接口;
- 重新定义 clone 方法,并指定 public 访问修饰符。
Cloneable接口只是个标签接口,不含任何需要实现的方法:
public class Employee implements Cloneable {
public Employee clone() throws CloneNotSupportedException {
Employee cloned = (Employee) super.clone();//对象
cloned.hireDay = (Date) hireDay.clone();//对象子对象
return cloned;
}
}
调用super.clone()
得到的是当前调用类的副本,而不是父类的副本。根本没有必用调用this.clone()
,并且也用不了 this.clone()
因为这里已经重写了。
lambda 表达式
lambda 的好处
lambda 表达式是一个可传递的代码块,可以在以后执行一次或多次。
有些地方,要将一个代码块传递到某个对象,这个代码块会在将来某个时间调用。在 Java 中传递一个代码段并不容易,不能直接传递代码段。Java 是一种面向对象语言,所以必须构造一个对象,这个对象的类型需要有一个方法能包含所需的代码。
Java SE 8 有了好方法来处理代码块。
lambda 表达式的语法
简单的 lambda 表达式:
(String first, String second)
-> first.length() - second.length()
也可以像写方法一样,把这些代码放在{}
中,并包含显式的return
语句:
(String first, String second) -> {
if(first.length() < second.length()) return -1;
else if(first.length() > second.length()) return 1;
else return 0;
}
即使没有参数,任然要提供空括号:
() -> {
for (int i = 100; i >= 0; i--){
System.out.println(i);
}
}
如果可以推导出一个 lambda 表达式的参数类型,则可以忽略其类型:
Comparator<String> comp = (first, second) ->
first.length() - second.length();
//编译器可以推导出 first 和 second 必然是字符串,
//因为 lambda 表达式将赋给一个字符串比较器。
如果方法只有一个参数,并且类型可以推出省略,那么可以省略参数括号:
ActionListener listener = event ->
System.out.println("The time is " + new Date());
无需指定 lambda 表达式的返回类型。它的返回类型会有上下文推到得出。
如果一个 lambda 表达式在一个分支返回一个值,另外一些分支不返回值,这是不合法的:
(int x) -> {
if(x >= 0) return 1;//不合法
}
函数式接口
对于只有一个抽象方法的接口,需要这种接口的对象时,可以提供一个 lambda 表达式,这种接口成为函数式接口。
Arrays.sort(words,
(first, second) -> first.length() - second.length());
lambda 表达式可以转换为接口:
Timer t = new Timer(1000, event -> {
System.out.println("At the tone, the time is " + new Date());
Toolkit.getDefaultToolkit().beep();
})
java 没有增强函数类型,所以不能声明函数类型。
方法引用
可能已经有现成的方法而已完成要传递到其他代码的某个动作:
Timer t = new Timer(1000, event -> System.out.println(event));
Timer t = new Timer(1000, System.out::println);
System.out::println
是一个方法引用,它们是等价的。
主要有3种情况:
- object::instanceMethod 如,
System.out::println
等价x -> System.out.println(x)
- Class::staticMethod 如,
Math::pow
等价(x, y) -> Math.pow(x, y)
- Class::instanceMethod 如,
String::compareTolgnoreCase
等价(x, y) -> x.compareTolgnoreCase(y)
也可以在方法引用中,使用this
和super
参数:
super::instanceMethod
this::instanceMethod
构造引用
Person::new
是Person
构造器的一个引用,用那个构造器取决于上下文
变量作用域
lambda 表达式可以捕获外围作用域中变量的值,lambda 表达式中只能引用变量的值而不能改变变量的值。另外在 lambda 表达式中引用变量,而这个变量可能在外部改变,这也是不合法的。
规则:lambda 表达式中捕获的变量必须实际上是最终变量。
lambda 表达式的体与嵌套块有相同的作用域。
lambda 表达式中的 this 关键字,指创建这个 lambda 表达式的方法的 this 参数。
处理 lambda 表达式
使用 lambda 表达式重点是延迟执行。
repeat(10, () -> System.out.println("Hello, World!"));
public static void repeat(int n, Runnable action){
for(int i = 0; i < n; i++) action.run();
}
常用的函数式接口:
略 240 页
内部类
内部类是定义在另一个类中的类,使用原因:
- 内部类方法可以访问该类定义所在的作用域中的数据,包括私有数据。
- 内部类可以对同一个包中的其他类隐藏起来。
- 想要定义一个回调函数且不想编写大量代码时,使用匿名内部类很便捷。
使用内部类访问对象状态
内部类方法可以访问自身的数据域,也可以访问创建它的外围类对象的数据域,包括私有数据。
public class Outer {
int num = 10;
class Inner{
int count = 20;
public void print(){
System.out.println("直接访问外部类属性"+num);
}
}
public void show(){
System.out.println("外部类。。。");
System.out.println("在外部类访问内部类属性" + new Inner().count);
System.out.println("在外部类访问内部类方法:");
new Inner().print();
}
public static void main(String[] args) {
new Outer().show();
System.out.println();
System.out.println("main。。。");
System.out.println("在mian中访问内部类属性" + new Outer().new Inner().count);
System.out.println("在mian中访问内部类方法:");
new Outer().new Inner().print();
}
}
内部类的特殊语法规则
内部类访问外部类的复杂形式:Outer.this.属性
外部类访问内部类的形式:Inner inner = new Inner()
其他类访问外部类中的内部类的形式:Outer.Inner inner = new Outer().new Inner()
- 内部类中声明的静态域必须是 final
- 内部类不能有 static 方法。也允许有,但只能访问外围类的静态域和方法。
私有内部类
class Out {
private int age = 12;
private class In {
public void print() {
System.out.println(age);
}
}
public void outPrint() {
new In().print();
}
}
public class Demo {
public static void main(String[] args) {
//此方法无效
/*
Out.In in = new Out().new In();
in.print();
*/
Out out = new Out();
out.outPrint();
}
}
如果一个内部类只希望被外部类中的方法操作,那么可以使用private声明内部类。
上面的代码中,我们必须在Out类里面生成In类的对象进行操作,而无法再使用Out.In in = new Out().new In() 生成内部类的对象。
也就是说,此时的内部类只有外部类可控制;如同是,我的心脏只能由我的身体控制,其他人无法直接访问它。
局部内部类
局部内部类可以对外界完美的隐藏起来。除了 Print 方法没人知道这个内部类。
我们将内部类移到了外部类的方法中,然后在外部类的方法中再生成一个内部类对象去调用内部类方法,这就是局部内部类。
class Out {
private int age = 12;
public void Print(final int x) {
class In {
public void inPrint() {
System.out.println(x);
System.out.println(age);
}
}
new In().inPrint();
}
}
public class Demo {
public static void main(String[] args) {
Out out = new Out();
out.Print(3);
}
}
由外部方法访问变量
局部内部类,不仅能够访问包含他们的外部类,还可以访问局部变量,但必须被声明为 final。
匿名内部类
加入只创建这个类的一个对象,就不必命名了,这种了被称为匿名内部类。
类名可以是一个接口,于是内部类就是实现这个接口;也可以是一个类,于是内部类就是对这个类扩展。
abstract class Person {
public abstract void eat();
}
public class Demo {
public static void main(String[] args) {
Person p = new Person() {
public void eat() {
System.out.println("eat something");
}
};
p.eat();
}
}
- 匿名类没有类名,所以类名没有构造器。
- 构造器参数会传递给超类构造器。内部类实现接口的时候,不能有任何构造器。
- 构造参数后面加个
{}
就代表是匿名内部类。
静态内部类
有时,使用内部类知识为了把一个类隐藏在另外一个类的内部,并不需要内部类引用外围类对象。可以将内部类声明为 static,以便取消产生的引用。
如果用 static 将内部内静态化,那么内部类就只能访问外部类的静态成员变量,具有局限性。
其次,因为内部类被静态化,因此Out.In
可以当做一个整体看,可以直接new
出内部类的对象(通过类名访问static
,生不生成外部类对象都没关系)。
class Out {
private static int age = 12;
//静态内部类
static class In {
public void print() {
System.out.println(age);
}
}
}
public class Demo {
public static void main(String[] args) {
Out.In in = new Out.In();
in.print();
}
}
- 只有内部类可以声明为 static 。
- 内部类只能访问外围类的静态成员变量,具有局限性。
- 静态内部类可以有静态域和方法。
- 声明在接口中的内部类自动成为
public static
类。
代理
利用代理可以在运行时创建一个实现了一组给定接口的新类。
只有在编译时无法确定需要实现那个接口时才有必要使用。
何时使用代理
有一个便是接口的 Class 对象,要想构造一个实现这些接口的类,需要使用 newInstance 方法或反射找出这个类的构造器。但是,不能实例化一个接口,需要在程序处于运行状态时定义一个新类。
代理类可以在运行时创建全新的类。这样代理类能够实现指定的接口:
- 指定接口所需要的全部方法。
- Object 类中的全部方法。
但不能运行时定义这些方法的新代码,而要提供一个调用处理器。
调用处理器是实现了 InvocationHandler 接口的类对象,只有一个方法:
Object invoke(Object proxy, Method method, Object[] args)
创建代理对象
使用 Proxy 类的 newProxyInstance 方法创建一个代理对象,它有三个参数:
- 一个类加载器。可以使用不同的类加载器,null 表示使用默认的类加载器。
- 一个Class对象数组,每个元素都是需要实现的接口。
- 一个调用处理器。现了 InvocationHandler 接口的类对象
使用代理的原因:
- 路由对远程服务器的方法调用。
- 在程序运行期间,将用户接口事件与动作关联起来。
- 为调试、跟踪方法调用。
使用代理和调用处理器跟踪方法调用,并且定义了一个 TraceHander 包装器类存储包装的对象。其中的 invoke 方法打印出被调用方法的名字和参数,随后用包装好的对象作为隐式参数调用这个方法:
public class TraceHandler implements InvocationHandler{
private Object target;
public TraceHandler(Object t){
target = t;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.print(target);
System.out.print("." + method.getName() + "(");
if (args != null){
for (int i = 0; i < args.length; i++) {
System.out.print(args[i]);
if (i < args.length - 1) System.out.print(",");
}
}
System.out.println(")");
return method.invoke(target, args);
}
public static void main(String[] args) {
Object[] elements = new Object[100];
for (int i = 0; i < elements.length; i++) {
Integer value = i + 1;
InvocationHandler handler = new TraceHandler(value);
Object proxy = Proxy.newProxyInstance(null, new Class[] {Comparable.class}, handler);
elements[i] = proxy;
}
Integer key = new Random().nextInt(elements.length) + 1;//随机生成一个数 [1, i + 1]
int result = Arrays.binarySearch(elements, key);//二分搜索法来搜索指定数组,以获得指定对象的位置
if (result >= 0) System.out.println(elements[result]);
}
}
只要 proxy 调用了某个方法,这个方法 method 的名字和参数 args 就会打印出来。
代理的特性
代理类是在程序运行过程中创建的,一旦被创建就变成了常规类,与虚拟机中的任何其他类没有什么区别。
代理类都扩展于 Proxy 类。一个代理类只有一个实例域(调用处理器)。任何附加数据都必须存储在调用处理器中。
代理类都覆盖了 Object 类中的方法。覆盖的方法,仅仅调用了调用处理器的 invoke。有些没有被重写定义,如 clone 和 getClass。
没有定义代理类的名字,虚拟机将会自动生成一个类名。
特定的类加载器和预设的一组接口,只能有一个代理类。调用两次 newProxyInstance 方法也只能得到同一个类的两个对象,利用 getProxyClass 方法可以获得这个类:
java Class proxyClass = Proxy.getProxyClass(null, interfaces);
- 代理类一个是 public 和 final。如果代理类实现的接口都是 public ,代理类就不属于某个特定的包;否则,所有非公有的接口都必须属于同一个包,同时,代理类也属于这个包。通过 Proxy 中的 isProxyClass 方法检测一个特定的 Class 对象是否代表一个代理类。
java System.out.println(Proxy.isProxyClass(elements[3].getClass()));
以上是关于Java核心技术卷一 4. java接口lambda内部类和代理的主要内容,如果未能解决你的问题,请参考以下文章
关于JAVA核心技术(卷一)读后的思考(接口的基本知识的讨论)
java 核心技术卷一笔记 6 .1.接口 lambda 表达式 内部类
java 核心技术卷一笔记 6 .1接口 lambda 表达式 内部类