Java笔记7
这章主要写接口,内部类和Lambda表达式。
抽象类
假定有个Shape类,需要一个calPerimeter()方法计算各种图形的周长,但是在这个类中显然不可能将所有图形的计算周长的方法全都实现,具体的应该交给各个子类自己实现。在父类中就需要一个抽象方法,具体的方法留待子类实现。
在抽象类中可以没有抽象方法,但是抽象方法必须在抽象类中。具体遵循下面的规则:
抽象方法和抽象类必须使用
abstract
修饰抽象类不能被实例化,即使里面并没有抽象方法
抽象类中可以包括普通类的各种成分(包括构造器,即使它不能被实例化,它的构造器主要是用于被子类继承)
含有抽象方法的类必须被定义成抽象类,具体包括:直接定义了一个抽象方法的类;继承了一个抽象父类但是在这个子类中并没有完全实现父类的抽象方法(即还剩抽象方法);或实现了一个接口但没有完全实现接口包含的抽象方法。
具体代码我这里偷个懒。
关于abstract和其他关键字的配合使用要注意,final永远不能和其一起,因为final表示不能被重写。abstract也不能用于修饰成员变量,局部变量和构造器。在和static配合使用时,不可以同时修饰某个方法,调用一个类中的没有方法体的方法肯定会出错;但是在调用一个抽象类中的静态方法是可以的;它们两个可以同时修饰内部类。private和abstract的使用也是,不能同时修饰某个方法。
不论如何搭配使用,最简单的规则在于不能违反抽象的初衷,不能妨碍子类的重写,就比如private修饰的方法如果加上abstract,子类无法访问到也就无法进行重写,显然是不合适的。
关于抽象还有一个参考链接,写的不错:https://blog.csdn.net/wei_zhi/article/details/52736350
接口
抽象类是从多个类中抽象出的模板,对抽象类更彻底的抽象就是“接口”(interface),接口里的方法全是抽象方法,而在Java8中允许在接口中定义默认方法。
关于什么是接口,接口的作用我也说不好,贴出个知乎链接,前几个回答写的都很好;https://www.zhihu.com/question/20111251
接口的定义方式:
1 | [修饰符] interface 接口名 extends 父接口1,父接口2... |
修饰符可以是public或者默认的包访问控制权限,接口的命名与类的命名类似,一个源文件中只能有一个public的接口,且名字应和源文件名字相同。
由于接口定义的是一种规范,是完全抽象出来的东西,因此不能有构造器和初始化块。作为规范,接口是公共的,所以接口里的东西都是公共的,不论是否加上了public修饰符。
对于接口中常量,系统会自动加上public static final这三个修饰符,同时,这也意味着只能在定义是指定默认值。
接口中定义的抽象方法在没有关键字abstract时也会自动加上,类方法和默认方法必须要有方法体。接口中的内部类,内部接口,内部枚举都采用public static修饰。
1 | public interface Output { |
对于默认方法,必须使用default修饰,不能使用static修饰,相对的接口中的类方法只能用static修饰,不能用default修饰。同样,他俩都会被加上public。
接口的继承
接口的继承不同与父类的继承,它支持一个接口继承多个直接父接口,通过extends关键字,之间用逗号隔开。
使用接口
接口不可以用来创建实例,但是可以用来声明引用类型变量,但是这个引用类型变量必须引用到接口的实现类的实现,意思是:假定用接口A声明引用类型变量C,那么这个C引用的对象必须是根据A实现的类B的对象。接口的主要用途还是被实现类来实现。用途:
- 定义变量或者用于强制类型转换
- 调用接口中的常量
- 被其他类实现
一个类可以实现一个或多个接口。子类继承父类使用extends关键字,而实现接口用implements关键字,implements必须放在extends后。一个类使用了implements关键字后,必须实现这些接口中定义的全部抽象方法,否则该类将保留继承到的抽象方法,所以要定义为抽象类。
1 | interface Product{ |
从上面的程序可以看出,Product p=new Printer();
Product接口修饰了其实现类的对象,同理,Object也是如此。对象o也可以直接使用接口中的默认方法。
由于接口里的方法都是public的,所以在实现类中的方法也必须是public的。
接口和抽象类
接口和抽象类都具有一下特征:
- 接口和抽象类都不能被实例化,用于被其他类实现和继承
- 接口和抽象类都包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法
但是接口和抽象类有着非常大的区别:
接口是一种规范,最简单来说,它充当在多个模块中间的耦合标准。例如,接口A规定了需要实现哪些方法和具有的常量,那么实现类B就会根据接口实现,而另一个类C参照接口就知道可以调用B中的哪些方法。如果是系统与外界交互,那么接口就能告诉系统先实现什么方法,也告诉用户可以使用哪些方法。总之,接口的作用就是告诉一边要做什么,另一边能做什么。而抽象类则体现了模板的思想,算是一种中间产品。在这种根本思想的差别外还在用法上存在差别:
- 接口只能包含抽象方法和默认方法,抽象类则可以包含普通方法
- 接口不能定义静态方法(JDK8之前),抽象类可以。
- 接口中只能有静态常量,不能有普通成员变量,抽象类则两种都可以
- 接口中不能有构造器,抽象类则有,虽然不能用
- 接口中不能有初始化块,抽象类则可以
面向接口编程
工厂模式
假定程序中有个Computer类需要一个输出的设备,一般可以直接组合(即包含)一个Printer类(上面的代码)在里面,修改Computer类就可以,但是如果有多个类组合了Printer类的话,一旦Printer类要被替代,那就要修改所有类中的Printer类,非常麻烦。
但是假如在Computer中组合一个Output(接口)的类,那就会很方便。因为它需要的是一个符合Output接口规范的类,所以不论Printer类还是修改后的BatterPrinter类,只要符合规范就可以直接被使用。
1 | public class Computer{ |
再贴出Output的代码:
1 | public interface Output { |
同时,在Computer类中也不负责具体的Output接口的实现类的对象产生,这部分工作交给了一个Output工厂。
1 | public class OutputFactory{ |
在OutputFactory这个类中,getOutput的返回值是符合Output接口的Printer类。这个类被传入computer的构造函数,现在Computer类的对象c中就具有了Printer()对象。c.keyIn则调用Printer()对象的getData方法,将文字存储进去。c.print()则调用了out()方法。
如果需要改进Printer()方法,则只要将getOutput的返回值改为改进后的类的对象。不需要担心在调用c.keyIn方法时不兼容,因为getOutput的返回类型是Output的,computer的构造器接受的也是Output的,只要符合Output接口规范的类在里面都可以使用。
命令模式
接口还有一个妙用,请看代码:
1 | public interface Command{//Command接口,要求接受一个数组 |
1 | public class ProcessArray{ |
1 | public class CommandTest{ |
假定根据Command接口实现了PrintCommand和AddCommand类,一个具有打印数组的方法,一个具有累加数组的方法。根据ProcessArray类中的方法,就可以实现不同的方法。这样子就实现了ProcessArray类中的process方法与真正的处理方法的分离。
内部类
内部类主要有以下作用和区别:
- 当一个类脱离另一个类后就完全没有意义时,则可以作为内部类,这样有更好的封装
- 内部类可以直接访问外部类的成员,因为它自己也是外部类的成员,但是不能倒过类访问
- 匿名内部类更适合创建一次性的类
- 因为是外部类的成员,所以可以用private,protected,static修饰
- 非静态内部类不能拥有静态成员
非静态内部类
内部类可以放在类内任何地方,包括方法体中(局部内部类)。成员内部类是一种与成员变量类似的类成员,但局部内部类和匿名内部类则不是。
当编译含有内部类的外部类时,会产生两个class文件,一个外部类的,还有个内部类的,名字是外部类和内部类的名字用$
连起来,不管是静态还是非静态的内部类。
内部类的对象可以访问外部类的私有数据,这是因为内部类的对象保存了它所寄生的外部类的对象的引用,调用非静态内部类的实例方法时,必须有一个非静态内部类实例,而非静态内部类必须寄生在外部类实例中。
当非静态内部类的某个方法访问某个变量时,会先在改方法中查找变量,然后是内部类,最后是外部类。如果外部类和内部类的成员变量存在重名情况,访问外部类则要通过OuterClass.this.membername使用,内部类则是this.membername。
外部类是不能直接访问内部类的。因为内部类的存在是依附于外部类,但是反过来不一定,内部类外一定有外部类,但是外部类中不一定有内部类。所以,当要访问内部类时,要显式的创建内部类。
根据静态成员不能访问非静态成员的规则,外部类的静态成员不能访问非静态内部类的成员。非静态内部类中也不允许定义静态成员。
为什么非静态内部类中不可定义静态成员,是因为,对于外部类来说,内部类也就是个成员,和其他的成员变量没有区别。在前面说过,内部类和外部类的class文件是分开的,当加载类时也是分开加载的,先加载外部类,如果要实例化外部类的话再加载内部类。因此,如果要有内部类(可以当成一个普通的外部类方法),没有外部类实例是不行的。但是,JVM要求所有的静态变量要在对象创建之前加载这就是矛盾所在。也有从另一个角度看问题的解释:https://segmentfault.com/q/1010000000304968。
静态内部类
用static修饰的内部类变成了属于外部类所有,不再和外部类的实例绑在一起。
静态内部类可以包含静态成员。静态内部类可以访问外部的静态成员,但是外部的非静态成员还是不能访问。因为,静态内部类是寄生在外部类中的,并不和某个外部类对象产生关联,没有持有他的引用,也就找不到相关的成员变量。
在接口中也是可以定义内部类的,但只能是static的。
使用内部类
在外部类中使用内部类
在外部类中使用内部类没有什么特别的地方,直接在外部类中声明并使用内部类就行。
在外部类以外的地方
如果希望在外部类使用内部类(包括静态和非静态),则内部类不能有private。具体语法格式:OuterClass.InnerClass varName=new OuterInatance.new InnerConstructor()
,首先需要用内部类来声明一个内部类的引用变量,然后要在外部类实例中再new一个内部类实例。非静态内部类的构造器必须要在外部类的实例中调用。
如果需要创建内部类的子类,则尤其要注意上面一点。因为子类在调用自生构造器之前会调用父类的构造器,所以必须保证子类可以访问到父类的构造器,这就要求必须存在一个外部类对象。
1 | public class SubClass extends Out.In{ |
上面代码的意思就是,在调用SubClass类的父类In类的构造器时,需要一个外部类的实例。非静态内部类的子类不一定是内部类,但是同样要保留一个引用,该引用指向父类的外部类的对象。当内部类的子类的对象存在时,也一定存在外部类的对象。
在外部类以外使用静态类
创建静态内部类实例的语法和非静态内部类的语法是类似的:OuterClass.InnerClass varName=new OuterClass.InnerConstructor()
,最明显的区别就是不需要通过外部类的实例调用内部类的构造器了。同样,内部类的子类的使用也更简单,无需外部类实例。
一点小疑问
内部类是外部类的一个成员,那么能否像重写方法一样在外部类的子类中重写内部类呢?是不能的。因为前面写过,内部类的类名不只是它的名字,而是外部类和内部类用’$’连起来的,父类的内部类名字是OuterA$InnerB.class,子类的名字就可能是OuterC$InnerB.class,它们实际不重名。
局部内部类
如果把内部类放在方法里,那就是局部内部类。它的上一级单元是方法,所以用static修饰这种内部类是没有意义的。与局部变量类似,局部内部类也不能用static和public等修饰。关于局部内部类的所有操作都和局部变量类似,只能在方法内部有用。请看代码:
1 | package test5; |
有意思的是生成的class文件,针对局部内部类和其子类,它们的生成class中间都有个数字,这是因为一个类中可能存在多个方法语句,每个语句中有种同名的局部内部类,所以需要加以区分。
匿名内部类
定义匿名内部类的格式:
1 | new 实现接口() | 父类构造器(实参列表){ |
直接给出代码:
1 | /** |
是不是觉得奇怪,怎么和上面的语法格式不太一样?匿名内部类因为使用了new关键字,所以会创建对象,只不过是没有引用变量显式的指向它,而且,匿名内部类是实现接口或调用父类构造器的,然而这里却没有出现接口的实现类和子类的名字,所以是匿名的。可以简单理解为,在不实现接口或继承父类的情况下,创建基于它们的对象。
再来看上面的代码,test()方法需要一个Product接口实现类的对象,但是因为内容都很简单,为此实现一个类出来不划算,所以采用了匿名内部类。ta.test()接受了匿名内部类的对象。
关于匿名内部类有些注意点:
- 匿名内部类因为必须创建对象,所以不能是抽象的,即必须完全实现接口或父类的抽象方法。只能在接口和父类中二选一,也不支持多个实现或继承。
- 支持对父类的非抽象方法进行重写
- 匿名内部类不能定义构造器,但是可以有初始化块。同时,因为不能定义构造器,所以只有默认的无参数构造器,所以在
new Product()
中没有参数。但是,如果是父类的匿名内部类,那么就会有与父类相似的构造器,具有构造器的形参列表。
在Java8之前,局部内部类,匿名内部类访问的局部变量必须是final修饰的,Java8之后不需要这样了。在Java8中,这种变量可以使用final修饰,也可以不使用,但是都是按照final的方式来用的。因此被匿名内部类和局部内部类访问的局部变量是不能重新赋值的。
Lambda表达式
Lam表达式支持将代码块作为方法参数,允许使用更简洁的代码来创建只有一个抽象方法的接口(函数式接口)。
先再看下命令模式中的代码:
1 | public interface Command{//Command接口,要求接受一个数组 |
1 | public class ProcessArray{ |
1 | public class CommandTest{ |
1 | public class AddCommand implements Command{ |
但是上面的代码需要我分别实现各个操作过程。接着试着用匿名内部类来改写:
1 | public class CommandTest{ |
注意有两个process,ProcessArray和接口中的,是不一样的。ProcessArray的对象的process方法接受数组和Command接口的实现类的对象,然后调用这个对象的process方法处理数组。而用匿名内部类时,直接根据接口创建一个对象。这样可以避免不必要的接口实现。
而使用Lambda可以将匿名内部类进一步简化:
1 | public class CommandTest{ |
从上面的代码来看,Lambda表达式仍然是创建了一个对象,只是省略了非常多的东西,不需要new Xxx(){},也不需要指出重写的方法的名字和返回值类型(上面的代码没用到),只要括号和括号中的参数列表。
形参列表允许省略参数类型,如果只有一个参数,连括号也可以省略。代码块如果只要一条语句,花括号也可以省略。Lambda代码块只有一条return语句。如果,代码块中只有一条省略了return的语句,那就会直接返回这条语句的值。
先看代码:
1 | interface Eatable { |
先来看看lq.eat(() -> System.out.println("苹果的味道不错"));
这句的意思,在LambdaQs的类体中查看eat()方法,这个方法需要一个Eatable接口的实现类的对象,下面的也是。Lambda表达式实际上会被当成一个任意类型的对象,具体是何种类型取决于运行环境的需要。但是这里面具体是如何运行的呢?
Lambda表达式与函数式接口
Lambda表达式的类型(也被叫“目标类型”),必须是“函数式接口”,这种接口只有一个抽象方法,但是可以包含多个默认方法,类方法。Java8还提供了@FunctionalInterface注解。
以Java本身提供的Runnable接口为例:
1 | //Runable接口中只包含一个无参数的方法 |
Lambda表达式实现的是特定函数式接口中的唯一方法。注意这句话的意思。
1 | Object obj = () -> { |
如果这样写,就会得到错误,因为Object不是一个函数式接口。如果要保证有一个明确的函数式接口,有三种常见方式:
- 将Lambda表达式赋值给函数式接口类型的变量
- 将Lambda表达式作为函数式接口的参数传给某个方法
- 使用函数式接口对Lambda表达式进行强制类型转换
1 | Object obj = (Runnable)() -> { |
接着回过头再来看一下Lambda表达式到底是如何来的。
Lambda的演变
最初的命令模式
1 | public interface Command{//Command接口,要求接受一个数组 |
1 | public class ProcessArray{ |
1 | public class CommandTest{ |
1 | public class AddCommand implements Command{ |
使用匿名内部类
1 | public class CommandTest{ |
匿名内部类不过是把接口中的抽象方法实现,但是没有具体的类而已。
Lambda表达式的演变
- 把外面的壳子去掉
只保留参数列表和方法题。参数和方法题之间用‘->’链接。为什么可以去掉?因为特定函数式接口中的唯一方法这句话,只有一个方法,当然可以知道省略的是哪个。
1 | public class CommandTest{ |
- 把return和{}去掉
这里不能去,跳过
- 把参数类型和圆括号去掉
只有一个参数时可以去掉,类似int[] target这样的,括号也不能去。
1 | public class CommandTest{ |
- 直接把表达式传递进去或者作为参数传递进去
1 | public class CommandTest{ |
再把变量名字改一下就和上面的一样了(上面的没省略变量类型)。这种演变分的方法请看链接http://how2j.cn/k/lambda/lambda-lamdba-tutorials/697.html#nowhere,非常不错的教程。
仔细观察发现,要想使用Lambda表达式,最重要的就是Lambda实现的匿名方法的参数列表要和函数式接口中唯一的抽象方法的接口一样。这意味这在前面提到过的Runnable强制转换可以用来改变Lambda表达式的用处,只要参数列表是一样的,就可以通过强制转换通用。</br?
Lambda表达式的本质是用简洁的语法来创建函数式接口的实例。
方法引用和构造器
当Lambda表达式只有一条语句时还可以使用方法引用和构造器引用。
引用类方法
定义了如下函数式接口:
1 |
|
该接口的要求是传入一个字符串并将其转化为Integer类型。分析后发现,不需要单独将实现类写出来,因为可以直接使用Integer.valueOf
方法转化。下面的代码使用Lambda表达式创建一个Converter接口的对象。
1 | Converter converter1 = from -> Integer.valueOf(from); |
接下来使用这个对象:
1 | Integer val = converter1.convert("99"); |
由于Lambda表达式创建了一个Converter对象,这个对象具有Lambda表达式代表的匿名方法,所以可以将参数传进去调用这个对象的方法。
对于这里的Lambda表达式,引用了Integer类的valueOf()类方法,所以还可以换种方式:
1 | Converter converter1 = Integer::valueOf; |
乍看肯定会说卧槽,这是怎么变得。我觉得要是直接从接口写出来这种有点转不过弯,还是一步步来吧。上面的类引用方法简单来说就是使用某个类的类方法来实现接口中的抽象方法。
引用特定对象的实例方法
现在还是用上面的那个接口,只不过要求变了,要求输入一个字符串,然后返回在给定字符串中的索引值(Integer)。
1 | Converter converter2 = from -> "hellojava".indexOf(from); //使用现成的indexOf方法 |
原本indexOf方法返回的是int类型,这里自动装箱为Interger。
1 | Integer val = converter2.convert("ll"); |
结果为2,也可以采用下面的简写:
1 | Converter converter1 = "hellojava"::indexOf; |
上面的实例引用方法就是使用具体的实例的方法来实现接口中的抽象方法。
引用某类对象的实例方法
这回换一个接口:
1 |
|
根据test()抽象方法,使用Lambda表达式创建一个MyTest对象:
1 | MyTest mt = (a,b,c) -> a.substring(b,c); //截取a字符串,从b开始,到c结束,不包括c |
1 | String str = mt.test("hello java",2,4); |
该代码的Lambda部分可以改写为:
1 | MyTest mt = String::substring; |
这里与上面类似,只不过不是具体的哪个对象的方法,而是某类对象的实例方法。注意和引用类方法的区别,从a.substring(b,c)
可以看出,它是实例方法。传入的三个参数,第一个作为调用者,后面两个作为substring的参数。
引用构造器
再换一个接口
1 |
|
该抽象方法要求根据String参数返回一个JFrame对象,Lambda代码如下:
1 | Yourtest yt = (String a) -> new JFrame(a); |
1 | JFrame jf = yt.win("我的窗口"); |
进一步简略:
1 | Yourtest yt = JFrame::new; |
不需要写明调用哪个构造器,就如同正常调用构造器一样,是根据传入的参数来决定用哪个构造器。
Lambda表达式和匿名内部类的联系和区别
通过上面一系列的使用,可以发现匿名内部类和Lambda表达式非常像,Lambda可以说是匿名内部类的简化版。
存在如下相同点:
- 都可以直接访问局部变量(之后不能改变)和外部类的成员变量
- 都可以直接调用从接口继承的默认方法
也有很多区别:
- 匿名内部类可以为任意接口创建实例,只要实现全部抽象方法即可;Lambda表达式只能为函数式接口创建实例
- 匿名内部类可以为抽象类或者普通类创建实例,但是Lambda表达式都不能
- 匿名内部类实现的抽象方法允许调用接口中的抽象方法,但是Lambda表达式不允许
一点补充知识
在Java8之前,局部内部类,匿名内部类访问的局部变量必须是final修饰的,Java8之后不需要这样了。在Java8中,这种变量可以使用final修饰,也可以不使用,但是都是按照final的方式来用的。因此被匿名内部类和局部内部类访问的局部变量是不能重新赋值的。
存在如下相同点:
- 都可以直接访问局部变量(之后不能改变)和外部类的成员变量
- 都可以直接调用从接口继承的默认方法
这两个地方都写到,局部内部类,匿名内部类,Lambda表达式访问外面的局部变量时,这些变量要是final的,即使没有这么写,之后也不能更改。大概的原因是,外部的局部变量会进行值传递,如果内部修改或外部修改值就会造成数据混乱。具体请看链接:
https://www.zhihu.com/question/21395848
http://cuipengfei.me/blog/2013/06/22/why-does-it-have-to-be-final/
https://www.cnblogs.com/hapjin/p/5744478.html
还有一篇关于闭包的:
https://www.zhihu.com/question/24084277/answer/110176733