第6章 接口、lambda表达式与内部类

接口

在Java程序设计语言中,接口不是类,而是对希望符合这个接口的类的一组需求。
接口中所有方法都自动是public方法。因此在接口中声明方法时,不必提供关键字public。但是在实现接口时必须把方法声明为public。
提供实例字段和方法实现的任务应该由实现接口的那个类来完成。可以将接口看成是没有实例字段的抽象类。

为了让类实现一个接口需要完成两个步骤:

  1. 将类声明为实现给定的接口。
  2. 对接口中的所有方法提供定义。
    需要使用关键字implements
class Employee implements Comparable

也可以为其提供一个类型参数

public class Employee implements Comparable<Employee>

在对有接口的类进行继承时,要注意“反对称”规则。如果子类要对父类的接口方法进行覆盖,那么就必须做好准备比较子类和父类,而不能仅仅使用强制类型转换。
如果不同子类中的同名接口方法有不同的含义,就应该将属于不同类的对象之间的比较视为非法。因此在方法开始前应该进行以下检测:

if(getClass()!=other.getClass()) throw new ClassCastException():

如果存在一个能够比较子类对象的通用算法,那么可以在超类中提供一个compareTo方法,并将这个方法声明为final。

接口的属性

接口不是类。不能用new运算符实例化一个接口。但是可以声明接口的变量。接口变量必须引用实现了这个接口的类对象。

Comparable x;
x=new Employee(...);

也可以从通用性较高的接口扩展到专用性较高的接口,和建立类的继承层次一样。
接口中不能包含实例字段,但是可以包含常量。接口中的字段总是public static final

默认方法

可以为接口方法提供一个默认实现,必须用default修饰符标记这样一个方法。

default int compareTo(T other){retern 0;}

默认方法可以调用其他方法。
“类优先”原则:接口的默认方法和超类中定义的方法产生冲突,只会考虑超类方法。

对象克隆

为一个包含对象引用的变量建立副本时:原变量和副本都是同一个对象的引用,任何一个变量改变都会影响另一个变量。

var original=new Employee("John Public",50000);
Employee copy=original;
copy.raiseSalary(10);//oops--also changed original

如果希望copy是一个新对象,它的初始状态与original相同,但是之后它们各自会有自己不同的状态,这种情况下就要使用clone方法

Employee copy=original.clone();
copy.raiseSalary(10);//OK--original unchanged
Object类如何实现clone

它对于这个对象一无所知,所有只能逐个字段地进行拷贝。如果对象中的所有数据字段都是数值或其他基本类型,拷贝这些字段没有任何问题。但是如果对象包含子对象的引用,拷贝字段就会得到相同子对象的另一个引用,这样一来,原对象和克隆的对象仍然会共享一些信息。默认的克隆操作是“浅拷贝”,并没有克隆对象中引用的其他对象。

注意
  • Object类中的clone方法声明为protected,子类只能调用受保护的clone方法来克隆它自己的对象。必须从新定义clone为public才能允许所有方法克隆对象。
  • Cloneable接口的出现于接口的正常使用并没有关系。这个接口只是作为一个标记,指示类设计者了解克隆过程。如果一个对象请求克隆,但是没有实现这个接口,就会生成一个检查型异常。
    即使浅拷贝实现能够满足要求,还是需要实现Cloneable接口,将clone重新定义为public,再调用super.clone()。

lambda表达式

lambda表达式是一个可传递的代码块,可以再以后执行一次或多次。(函数式编程)
可以了解一下函数式编程
为什么要使用lambda表达式?
在Java中,你不能直接传递代码段。Java是一种面向对象语言,所以必须构造一个对象,这个对象的类需要有一个方法包含所需的代码。

lambda表达式的语法

参数,箭头(->)以及一个表达式
可以像写方法一样,把表达式的代码放在{}中,并包含显式的return语句。
参数可以放在括号中,没有参数仍然要提供空括号。例如

()->{for(int i=100;i>=0;i--) System.out.println(i);}

如果可以推导出lambda表达式的参数类型,则可以忽略其类型。
如果方法只有一个参数,而且这个参数的类型可以推导得出,那么甚至还可以省略掉小括号。
无需指定lambda表达式的返回类型。

函数式接口

Java中有很多封装代码块的接口,如ActionListener或Comparator,lambda表达式于这些接口是兼容的。
对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式。这种接口称为函数式接口
lambda表达式可以转换为接口
例如:
Arrays.sort方法的第二个参数需要一个Comparator实例,Comparator就是只有一个方法的接口,所以可以提供一个lambda表达式:

//lambda表达式可以传递到函数式接口。
Arrays.sort(planets,(first,second)->first.length()-second.length());

在Java中,对lambda表达式所能做的也只是转换为函数式接口。甚至不能把lambda表达式赋给类型为Object的变量,Object不是函数式接口。

方法引用

对比lambda表达式和方法引用

//lambea 表达式转换为接口
var timer=new Timer(1000,event->System.out.println(event));

表达式System.out::println是一个方法引用,它指示编译器生成一个函数式接口的实例,覆盖这个接口的抽象方法来调用给定的方法。

var timer=new Timer(1000,System.out::println);

类似于lambda表达式,方法引用也不是一个对象。不过,为一个类型为函数式接口的变量赋值时会生成一个对象。

方法引用的三种情况
  1. object::instanceMethod
    方法引用等价于向方法传递参数的lambda表达式。对于System.out::print,对象是System.out,所以方法表达式等价于x->System.out.print(x)。
  2. Class::instanceMethod
    在这种情况下,第1个参数会成为方法的隐式参数。例如,String::compareToIgnoreCase等同于(x,y)->x.compareToIgnoreCase(y)。
  3. Class::staticMethod
    在这种情况下,所有参数都传递到静态方法:Math::pow等价于(x,y)->Math.pow(x,y)。

注意:只有当lambda表达式的体只调用一个方法而不做其他操作时,才能把lambda表达式重写为方法引用。
可以在方法引用中使用thissuper

变量作用域

lambda表达式可以捕获外围作用域中变量的值。在Java中,要确保所捕获的值是明确定义的,这里有一个重要的限制:在lambda表达式中,只能引用值不会改变的变量。
如果在lambda表达式中引用一个变量,而这个变量可能在外部改变,这也是不合法的。
lambda表达式中捕获的变量必须实际上是事实最终变量(这个变量初始化之后就不会再为它赋新值)。

几个需要注意的点:
  1. lambda表达式的体与嵌套块有相同的作用域,在lambda表达式中声明与一个局部变量同名的参数或局部变量是不合法的。
  2. lambda表达式中同样也不能有同名的局部变量。
  3. 在一个lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数。

内部类

内部类是定义在另一个类中的类

使用内部类访问对象状态

内部类的对象会有一个隐式引用,指向实例化这个对象的外部类对象。通过这个指针,它可以访问外部对象的全部状态。
一个内部类方法可以访问自身的数据字段,也可以访问创建它的外围类对象的数据字段。

内部类构造器

如果内部类没有定义构造器,编译器会为内部类生成一个无参数构造器。

public TimePrinter(TalkingClock clock)//这里TalkingClock是外部类
{
    outer=clock;
}

在外部类方法钟构造一个内部类对象后,编译器就会将当前外部类的this引用传递给这个构造器:

var listener=new TimePrinter(this);

内部类的特殊语法规则

表达式 OuterClass.this 表示外围类引用
一般来说,最新构造的内部类对象的外围类引用被设置为创建内部类对象的方法的this引用。不过,也可以通过显式地命名将外围类引用设置为其他的对象。例如:

var jabberer=new TalkingClock(1000,true);
TalkingClock.TimePrinter listener=jabberer.new TimePrinter();

在外围类的作用域之外引用内部类: OuterClass.InnerClass

要注意的地方
  • 内部类中声明的所有静态字段都必须是final,并初始化为一个编译时常量。如果这个字段不是一个常量,就可能不唯一。
  • 内部类不能有static方法。

为什么要使用内部类?

内部类可以访问外围类的私有数据,但是外部类则不行。
由于内部类拥有更大的访问权限,所以天生就比常规类更加强大。

局部内部类

如果一个类只在某个方法中创建这个类型的对象时使用了一次,可以在一个方法中局部地定义这个类。

public void start()
    {
        class TimePriter implements ActionListener //这是一个局部内部类
        {
            public void actionPerformed(ActionEvent event)
            {
                System.out.println("At the tone,the time is "
                        +Instant.ofEpochMilli(event.getWhen()));
                if(beep)Toolkit.getDefaultToolkit().beep();
            }
        }

        var listener=new TimePriter();
        var timer=new Timer(interval,listener);
        timer.start();
    }
  • 声明局部类时不能有访问说明符(public或private)。局部类的作用域被限定在声明这个局部类的块中。
  • 局部类有一个很大的优势,即对外部世界完全隐藏。除了定义它的这个方法以外,没有任何方法能够知道局部类的存在。

匿名内部类

假如只想创建这个类的一个对象,甚至不需要为类指定名字。
匿名内部类的语法

new SuperType(Construction parameters)
    {
        inner class methods and data
    }

SuperType可以是接口,也可以是一个类。如果是接口,内部类就要实现这个接口。如果是一个类,内部类就要扩展这个类。
因为构造器的名字必须与类名相同,而匿名内部类没有类名,所以,匿名内部类不能有构造器。实际上,构造参数要传递给超类(superclass)构造器。

匿名内部类使用技巧:“双括号初始化”

        var listener=new ActionListener()
            {
                public void actionPerformed(ActionEvent event)
                {
                    System.out.println("At the tone,the time is "
                    +Instant.ofEpochMilli(event.getWhen()));
                    if(beep) Toolkit.getDefaultToolkit().beep();
                }
            };
生成静态方法当前类的类名

一般我们使用getClass的时候,调用的是this.getClass(),而静态方法没有this。所以应该使用如下表达式:

new Object(){}.getClass().getEnclosingClass()//gets class of static method

在这里,new Object(){}会建立Object的匿名子类的一个匿名对象,而getEnclosingClass则得到其外围类,也就是包含这个静态方法的类。

静态内部类

如果在使用内部类的时候只是为了把一个类隐藏在另外一个类的内部,并不需要内部类有外围类对象的一个引用,可以将内部类声明为static。
静态内部类就类似于其他内部类,只不过静态内部类的对象没有生成它的外围类对象的引用。
如果在静态方法中构造内部类,则必须使用静态内部类。

静态内部类需要注意的地方
  • 只要内部类不需要访问外围类对象,就应该使用静态内部类。
  • 与常规内部类不同,静态内部类可以有静态字段和方法
  • 在接口中声明的内部类自动是static和public
访问静态内部类
//Pair类是ArrayAlg的一个公共内部类
ArrayAlg.Pair p=ArrayAlg.minmax(d);

服务加载器

代理

这两块知识点需要的话再回来学

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容