接口(interface)
- 接口用来描述类应该做什么,而不指定他们具体应该如何做。接口不是类,而是对符合这个接口的类的一组需求。
- 接口定义的关键词是interface
public interface Comparable
{
int compareTo(Object other);
}
- 接口是一个完全抽象的类,不会提供任何方法的实现,只会进行方法的定义。
- 接口中只能使用两种访问修饰符,一种是public,一种是缺省(包访问权限接口可以被其他类实现,实现接口的类需要提供接口的方法实现,一个接口可以有多个实现。)
- 接口中不能有实例字段变量,但是可以包含常量。换句话说,接口中的所有字段都是public static final的。
- 实现接口使用关键词implements。
- 接口不能被实例化,所以接口中没有任何构造方法,试图用new运算符实例化一个接口是错误的,但是却可以声明接口的变量,用接口的实现类来实例化
Comparable x=new Comparable()//错误
Comparable x=new Employee(...);//正确
class Employee implements Comparable
{
public int compareTo(Object otherObject)
{
Employee other=(Employee) otherObject;
return Double.compare(salary,other.salary);
}
}
- 使用instanceof检查一个对象是否实现了一个接口,同样的类似于类的继承,接口也可以实现继承。每个子类只能有一个超类,但是每个类却可以实现多个接口
class Employee implements Cloneable,Comparable
Employee同时实现Cloneable接口与Comparable接口。
- 接口的另一种思路:将方法声明放在抽象类中(抽象类的抽象方法本来就不实现),然后在子类中实现这些方法
这种思路的缺点在于:java不允许多重继承,一个类只能继承一个抽象类,也就是说一个类只能实现一个接口也,显然这不太好。 - 静态方法,只能通过接口名调用,不可以通过实现类的类名或者实现类的对象调用。default方法,只能通过接口实现类的对象来调用,当然如果接口中的默认方法不能满足某个实现类需要,那么实现类可以覆盖默认方法。
- 接口的静态方法
具体代码实现如下:
public interface Path
{
public static Path of(URI uri){...}
public static Path of(String first,String... more){...}
...
}
- 接口的默认方法
为接口提供一个默认实现,使用default关键词
public interface Comparable<T>
{
default int compareTo(T other)
{
return 0;
}
}
默认方法带来的好处,如同下面的代码而言,不需要操心remove()或者isEmpty()的实现问题。
public interface Iterator<E>
{
boolean hasNext();
E next();
deafult void remove()
{
...;
}
}
默认方法可以调用其他方法
public interface Collection
{
int size();// 一个抽象方法
default boolean isEmpty()
{
return size()==0;
}
}
默认方法可以实现接口演化:已经存在一个接口j,并且有一个类x实现了这个接口j,现在由于某些原因,在接口中又声明一个方法,但是不是一个默认方法,因为类x中没有这个方法,所以对类x的编辑会出错。
默认方法带来的问题
两个接口中同名方法带来的冲突
一个类既继承于一个接口,又继承于一个超类,且超类中有与接口中相同的方法,那么超类中的方法会覆盖掉接口的方法
public interface Person
{
default String getName()
{
return "";
}
}
public interface Named
{
default String getName()
{
return getClass().getName()+"_"+hashCode();
}
class Student implements Person,Named
{
public String getName()
{
//为什么这里不用this,用super,Person只是一个接口
return Person.super.getName();
}
}
comparator接口
String类已经实现了Comparable,但是String.compareTo方法按照字典顺序比较字符串,但现在我希望通过字符串长度去比较。
需要重新定义一个实现Comparator的类(一开始学的时候,总认为comparable与comparator是一个,原来是两个不同的接口,而comparator接口的功能更为强大)
在我现在的认识里,以及结合博客来看,Comparable接口与Comprator接口的最大差别在于Comparable是内部接口,Comparator是外部接口
//在真正写代码的时候,下面的接口定义是不用写的
//因为Comparator已经定义在java.util包中了
//我们只需要导入java。util.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.length()-second.length();
}
}
//具体使用时
String[] friends={"Peter","Paul","Mary"};
Arrays.sort(friends,new LengthComparator());
综上所述,排序接口的实现方式有两种
方法一、使用Comparable接口:
让待排序对象所在的类实现Comparable接口,并重写Comparable接口中的compareTo() 。方法缺点是只能按照一种规则排序。
方法二、使用Comparator接口 (推荐使用)
如果一个类要实现java.util.Comparator接口:它一定要实现
int compare(T o1, T o2) 函数,而另一个可以不实现(boolean equals(Object obj)。
使用编写排序方式类实现Comparator接口,并重写新Comparator接口中的compare()方法。优点:想用什么方式排就用什么方式排。
对象克隆
讨论Cloneable接口,我们先来看这一段代码
var original=new Employee("John Public",50000);
Employee copy=original;
copy.raiseSalary(10);
上面的的copy是original变量的一个副本,两者引用的是一个对象,任何对copy的修改都会造成original对象的改变。如果想要copy是一个与original初始状态相同的新的对象,就可以使用clone方法
Employee copy=original.clone();
copy.raiseSalary();
这样copy改变了,但是original却没有改变。但是这样的克隆只是“浅拷贝“,Object类在实现clone方法的时候,只能逐个字段的进行拷贝,但是如果遇到了对象包含子对象的引用,拷贝字段仍然得到的是相同子对象的一个引用。
如果想要实现clone,这个类必须实现Cloneable接口,重新定义clone方法,并指定public访问修饰符(因为所有类的父类Object中的clone方法虽然为protected的,但是子类只能调用受保护的clone方法克隆其自己的对象,如果想在类中随意的使用,必须将其声明为public)
对于一个类的实现者而言,需要有以下的思考:
- 默认的clone方法是否满足要求
- 是否可以在可变的子对象上调用clone来完成深拷贝
浅拷贝的例子
class Employee implements Cloneable
{
public Employee clone() throws CloneNotSupportedException
{
return (Employee) super.clone();
}
....
}
深拷贝的例子
class Employee implements Cloneable
{
public Employee clone() throws CloneNotSupportedException
{
Employee cloned=(Employee) super.clone();
cloned.hireDay=(Date)hireDay.clone();
return cloned;
}
....
}
lambda表达式
纯粹是为了编写代码时的简单高效。
lambda表达式允许你通过表达式来代替功能接口。lambda表达式就和方法一样,提供正常的参数列表和使用这些参数的主体(body,可以是一个表达式或代码块。
函数的方法的要素是:返回值、方法名、参数列表、方法体。
lambda表达式的要素:参数列表 方法体
定义:lambda表达式是一个可以传递的代码块,可以在以后执行一次或多次。
特点:将代码传递到某个对象。这个代码块会在将来的某个时间调用
lambda表达式的基本结构:(parameters) -> expression或者(parameters)->{statements;}
组成部分:
1.parameters:类似方法中的形参列表,这里的参数是函数式接口里的参数,这里的参数类型可以明确的声明 也可不声明而由JVM隐含的推断。另外当只有一个参数时可以省略掉圆括号
语法要点:
* 参数类型可以省略,如果需要省略,每个参数的类型都要省略。
* 参数的小括号里面只有一个参数,那么小括号可以省略
* 如果方法体当中只有一句代码,那么大括号可以省略
* 如果方法体中只有一条语句,且是return语句,那么大括号可以省略,且去掉return关键字。
(String first,String second)
->first.length()-second.length()
x->2*x
Comparator<String> comp=(first,second) ->first.length()-second.length()
写成方法形式(代码块之外要加大括号)
(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);}
()->2
忽略参数类型形式
函数式接口
java中有很多封装代码块的接口,并不是所有的接口都是函数式接口,只有当一个接口有且只有一个抽象方法(什么叫抽象方法啊:只定义方法声明,而不定义方法体的方法)时才叫做函数式接口。
定义方式
@FunctionalInterface
public interface Comparator{
int compare(Object o1,Object o2)
}
@FunctionalInterface
public interface test{
void test();
default void test2(){
System.out.println("我是一个默认方法");
}
}
lambda表达式与这些接口是兼容的,对于只有一个抽象方法的接口,需要这种接口的对象(往往就是=的右边,需要我们new 一个对象的时候)时,就可以提供一个lambda表达式,这种接口称为函数式接口。
Arrays.sort方法在底层会接受实现了Comparator的某个类的对象。
Arrays.sort(words,(first,second)->first.length()-second.length());
方法引用
- 方法引用是lambda表达式的一种特殊类型,通过现有方法来创建简单的lambda表达式。具体的下个定义就是说:方法引用是一个更加紧凑,易读的 Lambda 表达式,注意方法引用是一个 Lambda 表达式,其中方法引用的操作符是双冒号 “::”。
- 本质上,方法引用对于我们如果想改写某些已有的方法是没有帮助的。
- 只有当lambda表达式只调用一个方法而不做其他操作时,才能把lambda表达式重写为方法引用。
//做了两个操作,算长度,作比较
//所以不能使用方法引用
s->s.length()==0
方法引用与普通的lambda表达式可以相互转换
//下面的s其实如果在不知道具体的参数类型的时候,我们也没必要知道,但是这个情况可以从前面的推断得出
Consumer<String> printer = s -> System.out.println(s);
Consumer<String> printer=System.out::println;
//下面这种情况,我们几乎不得而知s是什么类型的
walker.walk(x->
{
x.forEach(s->{System.out.println(s);});
return null;
}
);
walker.forEach(s->{System.out.println(s);});
walker.forEach(System.out::println);
有几种方式:
-
对象引用::实例方法名
-
类名::静态方法名
-
类名::实例方法名
-
类名::new
-
类型[]::new
构造器引用
- 构造器引用即为上面的类名::new与类型[]::new
Java有一个限制,无法构造泛型类型T的数组。数组构造器(类型[]::new)会帮助消除这个限制。
在方法引用中还区分绑定的方法引用与未绑定的方法引用。
变量作用域
- 由于变量作用域的原因,lambda表达式可以捕获作用域中变量的值。这个变量一般就叫做自由变量。
- 代码块以及自由变量值的统称为:闭包(closure)
- 来看一段代码,其中
text
就是自由变量,这个变量并不在lambda表达式中,而是在外围的方法中。
public static void repeatMessage(String text,int delay)
{
ActionListener listener=event->
{
System.out.println(text);
Toolkit.getDefaultToolKit().beep();
};
new Timer(delay,listener).start();
}
- lambda表达式只能引用值不会改变的自由变量。
- lambda表达式中不能有同名的局部变量。
int e1=0;
Arrays.sort(envelopes,(e1,e2)->
{
if(e1[0]!=e2[0])
{
return e1[0]-e2[0];
}
else{
return e2[1]-e1[1];
}
});
- lambda中的
this
关键词指的是外围class的this对象。
处理lambda表达式 - 使用lambda表达式的重点在于延迟执行。具体的使用场景如下:
在一个单独的线程中运行代码
多次运行代码
在算法的适当位置运行代码(排序中的比较操作)
发生某种情况时执行代码(点击了一个按钮,数据到达等等)
Comparator与lambda表达式
内部类
- 定义:内部类是定义在另一类中的类。
- 注意:内部类不同于一个文件中多个类的其他类,必须是定义在类中的类(这里有一个小插曲:java一个文件中允许存在多个类,但是只有一个类被声明为public,编译器在编译的时候会分别生成多个类分别的字节码)
- 为什么要用内部类:内部类可以对一个包中的其他类隐藏
- 使用内部类访问对象状态:一个内部类既可以访问本类的数据字段,也可以访问创建它的外围类对象的数据字段。
- 内部类对象是由外部类的方法构造的
为什么内部类能直接使用外部类的实例字段呢?因为内部类的对象总有一个指向外部类对象的隐式引用。
外围类引用:OuterClass.this
表示外围类引用。
内部类对象的构造函数outerObject.new InnerClass(construction parameters)
在外部类的作用域之外,可以这样引用内部类:
OuterClass.InnerClass
编译器会将内部类转换为常规的类文件,用$区分外部类名与内部类名。
- 局部内部类
如果发现某个内部类只在一个方法中只出现了一次,那么可以在相应部位中的方法中创建这个内部类
声明局部类不能有访问说明符(public、private)
比如一个内部类就只在一个方法中声明使用,那么可以直接把类的声明全部放入该方法中即可。 - 匿名内部类
new SuperType(construction parameters)
{
inner class methods and data
}
匿名内部类不允许有构造器,匿名内部类常常用于实现继承与接口实现过程中方法的重写。
看一段Comparator接口重写的代码
Arrays.sort(envelopes,new Comparator<int[]>()
{
public int compare(int []e1,int []e2){
if(e1[0]!=e2[0])
{
return e1[0]-e2[0];
}
else{
return e2[1]-e1[1];
}
}
});
继承Thread类并实现其中的run方法
new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}.start();
- 静态内部类
如果只是希望将类隐藏再类的内部,而在内部类中不生成外部类的引用,或者并没有使用到外部类的实例字段。
可以将内部类声明为static类,也只有内部类才能被声明为static。
具体的讲解内部类的文章,可以查看我的博客中的另一篇。