吧Github代码链接: https://github.com/deyou123/corejava.git
第六章 接口、lambda表达式和内部类 6.1 接口 6.1.1 接口概念- 接口不是类,而是对类的一组需求描述。
- ex: Comparable接口
public interface Comparable{
int compareTo(Object other);
}
public interface Comparable{ int compareTo(T other); }
不带参数,compareTo是Object类型
-
接口中所有方法自动地属于public,不必提供关键字public
-
接口中可能包含多个方法。
-
接口中还可以定义常量。
-
接口不饿能提供哪些功能:
- 接口中绝对不能含有实例域
- Java SE8 之前不能在接口中实现方法;现在已经可以在接口中提供简单方法了;这些方法不能引用实例域。
-
可以将接口看成是没有实例域的抽象类。
-
为了让类实现一个接口,需要两个步骤
1、将类声明为实现给定的接口
2、对接口中的所有方法进行定义。 -
implements:
class Employee implements Comparable
public int compareTo(Object otherObj){
Employee other = (Employee)otherObj;
return Double.compare(salary,otherObj.salary);
}
- 在实现接口时,必须把方法声明为public;否则编译器会提出访问权限的警告信息。
- 接口不是类,不能new实例
- 可以生命接口变量;必须引用实现了接口的类对象
- 可以使用instanceof检查一个对象是否实现了某个特定的接口
if(obj instanceof Comparable){…} - 接口可以被扩展;允许存在多条从具有较高通用性的接口到较高专用性接口的链。
public interface Moveable{
void move(double x, double y);
}
public interface Powered extends Moveable
{
double milesPerGallon();
}
- 接口可以包含常量;接口中的域都将被自动设为 public static final
public interface Powered extends Moveable{
double milesPerGallon();
double SPEED_LIMIT = 95;
}
- Cloneable接口,clone方法可以创建类对象的一个拷贝。
- 每个类只能扩展于一个类
- Java SE8 中,允许存在接口中增加静态方法;只是有违将接口作为抽象规范的初衷。
- 通常的做法是将静态方法放在伴随类中;ex成对出现的接口和使用工具类,Collection/Collections 或 Path/Paths
- ex
public interface Path
{
public static Path get(String first,String... more){
return FileSystems.getDefault().getPath(first, more);
}
}
6.1.5 默认方法
- 可以为接口方法提供一个默认实现;default修饰符
- 实现接口的程序员只需要为他们真正关系的事件覆盖相应的监听器。
- 接口中一个默认方法,父类或者另一个接口中定义了同样的方法
1、父类优先,其他默认方法会被忽略。
2、接口冲突,必须覆盖这个方法来解决冲突。
回调:是一种常见的设计模式;可以指出某个特定事件发生时应该采取的动作;
ex:可以指出按下鼠标或者选择某个菜单项时应该采取什么行动。
public class TimerTest
{
public static void main(String[] args)
{
ActionListener listener = new TimePrinter();
// construct a timer that calls the listener
// once every 10 seconds
Timer t = new Timer(1000, listener);
t.start();
JOptionPane.showMessageDialog(null, "Quit program?");
System.exit(0);
}
}
class TimePrinter implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
System.out.println("At the tone, the time is " + new Date());
Toolkit.getDefaultToolkit().beep();
}
}
javax.swing.JOptionPane 1.2 static void showMessageDialog(Component parent, Objct message);显示一个包含一条消息和OK按钮的对话框,在parent或屏幕中间。 javax.swing.Timer Timer(int interval, ActionListenner listener);构造计时器,每个interval通告listenner一次。 void start(); 启动计时器;定时器将调用监听器的actionPerformed void stop(); 停止计时器; java.awt.Toolkit 1.0 static Toolkit getDefaultToolkit();:获得默认工具箱 void beep(); 发出铃响。6.2.2 Comparator接口 6.2.3 对象克隆
-
默认的克隆操作是 浅拷贝,不会克隆对象中的引用(String、对象等)。
-
对于每一个类,需要确定:
1、默认的clone方法是否满足要求;
2、是否可以在可变的子对象上调用clone来修补默认的clone方法;
3、是否不该使用clone -
对于1、2,类必须:
1、实现Cloneable接口;
2、重新定义clone方法,并指定public访问修饰符。 -
Cloneable接口(标记接口:tagging interface)的出现与接口的正常使用并没有关系;只是作为一个标记,指示类设计者了解克隆过程。
-
用途是确保一个类实现一个或一组特定的方法。
深拷贝例子:
class Employee implements Cloneable{
...
public Employee clone(0 throws CloneNotSupportedException{
Employee cloned = (Employee)super.clone();
cloned.hireDay = (Date) hirDay.clone();
return cloned;
}
}
- try-catch适用于final类; 保留throws说明符
- 当心子类的克隆;不能保证clone满足子类的需求;
- 所有的数组类型都有一个public的clone方法,可以用这个方法建立一个新数组。
int[] luckyNumbers = {...}
int[] cloned = luckyNumbers.clone();
cloned[5] = 12;
6.3 lambda表达式
6.3.1 为什么引入lambda表达式
- lambda表达式是一个可传递的代码块;可以在以后执行一次或多次。
- ex:
Class Worker implements ActionListener{
public void actionPerformed(ActionEvent event){//do some work;
}
}
class LengthComparator implements Comparator6.3.2 lambda表达式语法{ public int compare(String first,String second){ return first.length() - second.length();} } Arrays.sort(strings, new LengthComparator());
+ex:
(String first, String second)-> first.length() - second.length();
- 参数,箭头(->)以及一个表达式。
(String first, String second)->
{
if(first.length() < second.length())return -1;
else if(first.length() > second.length()) reutrn 1;
return 0;
}
- 即使lambda表达式没有参数,仍然要提供空括号:
()->{for(int i = 0; i < 100; i++)System.out.println(i);}
- 如果可以推导出lambda表达式的参数类型,则可以忽略其类型。
Comparatorcomp = (first,second)->first.length() - second.length();
first和second都是字符串
- 如果只有一个参数,并且这个参数类型可以推导得出,可以省略小括号:
ActionListener listener = event->System.out.println("The time is " + new Date());
- 无需只想lambda表达式的返回类型;lambda表达式的返回类型总是会由上下文推导得出。
(String first,Stirng second)-> first.length()-second.length();注释
- 如果lambda表达式只是在某些分支返回一个值,而在另外一个分支不返回值,这是不合法的。
(int x) -> {if(x >= 0) return 1;}//不合法
6.3.3 函数式接口
函数式接口:对于只有一个抽象方法的接口;可以提供一个lambda表达式。
Arrays.sort(words,(first,second)->first.length()-second.length());
- 在顶层,Arrays.sort会接受实现了Comparator< String >的某个对象;调用compare方法执行这个lambda表达式的体。
- 可能要高效得多;
- 最好把lambda表达式看作是一个函数,而不是一个对象。
- 要接受lambda表达式可以传递到函数式接口。
- lambda表达式可以转换为接口。
Timer t = new Timer(1000, event->
{
System.out.println("At the time is" + new Date());
Toolkit.getDefaultToolkit().beep();
});
- lambada表达式所能做的也只是能转换为函数值接口。
- java.util.funciton包中定义了很多分厂通用的函数式接口。
- Bifunction
描述了参数类型为T和U而且返回类型为R的函数;可以把我们的字符串比较lambda表达式保存在这个类型的变量中:
Bifucntioncomp = (first,second)->first.length() - second.length();
- 有用的接口Predicate:
public interface Predicate{ boolean test(T t); }
ArrayList类有一个removelf方法,参数就是一个Predicate;这个接口专门用来传递lambda表达式:
list.removelf(e -> e == null);
有时,可能已经有现成的方法可以完成你想要传递到其他代码的某个动作;
例如:希望只要出现一个定时器事件就打印这个事件对象:
Timer t = new Timer(1000, event->System.out.println(event));
但是如果把println方法传递到Timer构造器更好:
Timer t = new Timer(1000, System.out:: println);
表达式System.out::println是一个方法引用(method reference),它等价于lambda表达式 x->System.out.println(x);
再来一个例子:字符串排序
Arrays.sort(strings, String:: compareToIgnoreCase);
要用::操作符分割方法名与对象或类型
-
object::instacneMethod
-
Class::staticMethod
-
Class::instanceMethod
-
第二种情况,方法引用等价于提供方法参数lambda表达式。
- System.out::println => x-> System.out.println(x);
- Math::pow => (x,y)-> Math.pow(x,y);
-
第三种情况,第1个参数会成为方法的目标。
- String::compareToIgnoreCase => (x,y)->x.compareToIgnoreCase(y);
-
如果有多个同名的重载方法,编译器就会从上下文中找出你指的哪一个方法。
-
可以在方法引用中使用this参数;this::equals => x->this.equals(x).
-
使用super也是合法的: super::instanceMethod
- 构造器引用和方法引用很类似,只不过方法名为new。
- Persion::new 是Persion构造器的一个引用,用哪一个构造器取决于上下文。
- 假设有一个字符串列表,可以把它转换成一个Persion对象数组,为此要在各个字符串上调用构造器:
ArrayListnames = ...; Stream stream = names.stream().map(Persion::new); List people = stream.collect(Collectors.toList());
- map方法会为哥哥列表元素调用Person(String)构造器;如果有多个构造器,编译器会选择有一个String参数的构造器。
- 数组类型建立构造器引用:int[]:: new =》 x->new int[x].
- 无法构造泛型类型T的数组。表达式new T[n] 会改为 new Object[n]。
- 流库利用构造器引用,可以把Person[]::new传入toArray方法:
Person[] people = stream.toArray(Person::new);
toArray方法调用这个构造器来得到一个正确类型的数组;然后填充这个数组并返回。
希望能够在lambda表达式中访问外围方法或是类中的变量。
public static void repeatMessage(String text,int delay){
ActionListener listener = event->{
System.out.println(text);
Toolkit.getDefaultToolkit().beep();
};
new Timer(delay,listener).start();
}
repeatMessage("Hello", 1000);
- text变量是repeatMessage方法的一个参数变量。
- lambda表达式的代码可能会在repeatMessage调用返回很久以后才运行,而那时这个参数变量已经不存在了。
lambda表达式的3个部分:
- 一个代码块;
- 参数;
- 自由变量的值,这是指非参数而且不在代码中定义的变量。
lambda表达式捕获captured变量text;自由变量的值会复制到这个对象的实例变量中。
注释lambda表达式就是闭包(closure);
- lambda表达式可以捕获外围作用域中变量的值。
- 要确保所捕获的值是明确定义的;之恶能引用值不会改变给的变量;
public static void countDown(int start,int delay){
ActionListener listener = event->{
start--; //Error: can't mutate captured variable
Sout(start);
};
}
这个限制的原因:如果在lambda表达式中改变变量,并发执行多个动作时就会不安全。
- lambda表达式不能引用在外部可能会改变的变量。
for(int i = 0; i < 10; i++){
ActionListener listener = event->{
sout(i + " " );//Error
}
}
- lambda 表达式中捕获的变量必须实际上是最终变量;这个变量初始化之后就不会再为他赋新值。
- lambda表达式的体与嵌套块有相同的作用域;在lambda表达式中声明与一个局部变量同名的参数或局部变量是不合法的。
- lambda表达式中同样也不能有同名的局部变量
- 在一个lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法this参数。
public class Applicaiton{
public void init(){
ActionListener listener = event->
{
sout(this.toString());
}
}
}
表达式this.toString() 会调用Application对象的toString方法;而不是ActionListener实例的方法。
6.3.7 处理lambda表达式重点是延迟执行(deferred execution);
- 在一个单独的线程中运行代码:
- 多次运行代码:
- 在算法的适当位置运行代码(排序比较操作)
- 发生某种情况时执行代码 (点击事件)
- 指在必要时才运行代码。
ex:重复一个动作n次;将这个动作和重复次数传递到一个repeat方法:
repeat(10, ()-> sout("…"));
- 要接受这个lambda表达式,需要选择一个函数值接口。
public static void repeat(int n, Runnable action){
for(int i = 0; i < n; i++)aciton.run();
}
调用action.run()时会执行这个lambda表达式。
- 我们希望这个动作他出现在那一次迭代中;需要选择一个合适的函数值接口:包含一个方法,有一个int参数而且返回类型为void;
public interface IntConsumer{
void accept(int value);
}
repaet改进版:
public static void repeat(int n, intConsumer action){
for(int i = 0; isout("CountDown: " + (9 - i));
基本类型int、long、double的34个可能的规范;使用这些特殊规范来减少自动装箱。
-
最好使用上两表中的接口;假设要编写一个方法来处理满足某个特定条件的文件;对此有一个遗留接口 java.io.FileFilter,最好使用标准的Predicate
-
大多数标准函数式接口都提供了非抽象方法来生成或合并函数。
- ex:Predicate.isEqual(a) => a::equals,a为null也能正常工作。
- 提供了默认方法and、or和negate来合并谓词。Predicate.isEqual(a).or(Predicate.isEqual(b)) 等同 x->a.equals(x) || b.equals(x);
-
如果设计只有一个抽象方法的接口;可以用@FunctionalInterface注解来标记这个接口。
-
如果无意中增加了另一个非抽象方法,编译器会产生错误消息。
-
javadoc页里会指出你的接口是一个函数式接口。
6.3.8 再谈 Comparator
-
Comparator接口包含很多方便的静态方法来创建比较器;这些方法可以用于lambda表达式或方法引用。
-
静态comparing方法去一个”键提取器“函数,将类型T映射为一个可比较的类型。
Arrays.sort(people,Comparator.comparing(Person::getName)); -
可以把比较器与thenComparing方法串起来
Arrays.sort(people,
Comparator.comparing(Person:: getLastName).
thenComparing(Person::getFirstName); -
根据名字长度排序:
Arrays.sort(people, Comparator.comparing(Person::getName,
(s, t)->Integer.compare(s.length(), t.length())); -
避免int、long、double的装箱
Arrays.sort(people, Comparator.comparingInt(p->p.getName().length())); -
如果键函数可以返回null,要用到适配器:nullsFirst、nullsLast;可以修改现有的比较器,遇到null值时不会抛出异常,而是将这个值标记为小于或大于正常值。
ex: Comparator.comparing(Person::getMiddleName(),Comparator.nullFirst(…). -
nullFirst方法需要一个比较器,这里是比较两个字符串的比较器;
-
Arrays.sort(people, comparing(Person::getMiddleName,nullsFirst(naturalOrder())));
内部类:是定义在另一个类中的类
使用内部类:
- 内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据。
- 内部类可以对同一个包中的其他类隐藏起来
- 当想要定义一个回调函数且不想编写大量代码时,使用匿名内部类比较便捷。
- 一个方法可以调用这个方法的对象数据域。
- 内部类及可以访问自身的数据域,也可以访问创建它的外围类对象的数据域。
- 内部类的默认构造
public TimePrinter(TalkingClock clock){
outer = clock;
}
6.4.2 内部类的特殊语法规则
-
内部类有一个外围类的引用outer: OuterClass.this; ex: TalkingClock.this.beep
-
编写内部对象的构造器:outerObject.new InnerClass(construction parameters);
ex: ActionListener listener = this.new TimePrinter(); -
外围类的作用域之外,可以这样引用内部类:OuterClass.InnerClass
-
内部类中生命的所有静态域都必须是final;我们希望一个静态域只有一个实例。
可以在方法中定义局部类。
public void start(){
class TimePrinter implements ActionListener{
public void actionPerformed(Action event)
{
...
}
}
}
- 局部类不能用public或private访问说明符进行声明
- 他的作用域被限定在声明这个局部类块中。
- 可以对外部世界完全的隐藏起来
- 局部类不仅能够访问包含他们的外部类,还可以访问局部变量;局部变量为final。
- 匿名内部类: 加入之创建这个类的一个对象,就不必命名了;
public void start(int interval, boolean beep)
{
ActionListener listener = new ActionListener(){
public void actionPerformed(ActionEvent event){
sout...
...
}
};
Timer t = new Timer(interval,listener);
t.start();
}
- 创建一个ActionListener接口类的新对象,需要实现的方法actionPerformed定义在括号{}内。
new SuperTyep(construction parameters)
{
inner class methods and data;
}
- superType可以是ActionListener这样的接口,内部类实现这个接口;SuperType也可以是一个类,内部类扩展他。
- 匿名内部类不能由构造器。
- 将构造器参数传递给超类构造器
- 内部是实现接口的时候,不能有任何构造参数
new interfaceType(){
methods and data
} - 如果构造参数的闭小括号后面跟一个开大括号,正在定义的就是匿名内部类。
例子。。。
6.4.7 静态内部类- 又是只需要把一个类隐藏在另一个类的内部,并不需要内部类引用外围类对象。
- 可以将内部类声明为static,以便取消产生的引用。
- 只有内部类可以声明为static;
- 静态内部类的对象除了没有对生成他的外围类对象的引用特权外,与其他所有内部类完全一样。
- 利用代理可以再运行时创建一个实现了一组给定接口的新类。
- 只有在编译时无法确定需要实现那个接口时才有必要使用。
- 对于系统程序设计人员来说,地阿里带来的灵活性十分重要;不感兴趣的可以直接跳过。
- 指定接口所要的全部方法
- OBject类中的全部方法
调用处理器是是吸纳了InvocationHandler接口的类对象;有一个方法:
Object invoke(Object proxy,Method method,Object[] args);
- 无论何时调用代理对象的方法,调用处理器的invoke方法都会被调用,并向其传递Method对象和原始的调用参数。
- 调用处理器必须给出处理调用的方法。
使用Proxy类的newProxyInstancee方法创建代理对象,三个参数:
- 一个类加载器。
- 一个Clsss对象数组,每个元素都是需要实现的接口
- 一个调用处理器
使用代理处于很多原因:
- 路由对远程服务器的方法调用。
- 再程序运行期间,将用户接口事件和动作关联起来
- 为调试、跟踪方法调用
再
例子。。。
- 代理类是在程序运行过程中创建的;创建后变成常规类,和其它类没区别。
- 所有代理类都扩展于Proxy类;一个代理类只有一个实例域–调用处理器;他定义在Proxy的超类中。
- 所有的代理类都覆盖了Object类中的方法:toString、equals、hashCode;代理方法仅仅调用了调用处理器的invoke。
- 没有定义代理器的名字;SUn是¥Proxy开头的类名。
- 对于特定的类加载器和预设的一组接口来说,只能有一个代理类;
- 代理类一定是public和final;



