这里将介绍类在不同层次上的抽象方法。首先说明如果只有部分方法为抽象方法,那么这是一个抽象类,如果全部方法都为抽象方法,那么这是一个接口;然后描述抽象类与接口的使用差异;最后详速接口的相关用法,包括Java8对接口做了哪些扩展,以及如何通过匿名内部类简化接口的实现编码。
1. 抽象类
在Java编程中,_功能不确定的方法_被称作抽象方法,而包含抽象方法的类受到牵连就变成了抽象类。
抽象类的注意点:
- 被abstract修饰的抽象方法,由于_方法_的具体实现并_不明确_,因此抽象方法没有花括号所包裹着的方法体;
- 被abstract修饰的抽象类,由于包含至少一个抽象方法,因此不允许外部创建抽象类的实例;
- abstract只能修饰抽象方法和抽象类,不能修饰成员属性,因为属性值本身就允许通过赋值改变,无所谓抽象不抽象的;
- 虽然抽象类有构造方法,但它的构造方法并不能直接被外部调用,因为外部不允许通过构造方法来创建抽象类的实例化,抽象类的构造方法只能提供给子类调用。
以上的概念听起来很模糊,那接下来就用代码来实现理解。
首先,我们定义一个鸡的抽象类;
//演示抽象类的定义。这是一个抽象鸡类
abstract public class Chicken {
public String name; // 鸡的名称
public int sex; // 鸡的性别
// 定义一个抽象的叫唤方法。注意后面没有花括号,并且以分号结尾
abstract public void call();
// 即使抽象类定义了构造方法,外部也无法创建它的实例,只能被子类所重写
public Chicken() {
}
// Java只有抽象类和抽象方法,没有抽象属性的说法
// abstract public String cry;
}
然后,如果鸡类的抽象方法call()不被子类重写,那么子类仍为抽象类,只有被重写了子类才能具备所有完善的行为动作。
//定义一个继承自抽象鸡类的公鸡类
public class Cock extends Chicken {
public Cock() {
sex = 0; // 公鸡的性别固定为雄性
}
// 重写了公鸡的叫唤方法。如果不重写父类的抽象方法,那么该子类仍旧为抽象类
public void call() {
System.out.println("喔喔喔");
}
}
//定义一个继承自抽象鸡类的母鸡类
public class Hen extends Chicken {
public Hen() {
sex = 1; // 母鸡的性别固定为雌性
}
// 重写了母鸡的叫唤方法。如果不重写父类的抽象方法,那么该子类仍旧为抽象类
public void call() {
System.out.println("咯咯咯");
}
}
最后就是外部调用各种鸡类了,对于外部而言,唯一的区别是不能创建抽象类的实例。
//演示抽象类派生而来的子类用法
public class TestAbstract {
public static void main(String[] args) {
// 不能创建抽象类的实例,因为抽象类是个尚未完工的类
// Chicken chicken = new Chicken();
Cock cock = new Cock(); // 创建一个公鸡实例,公鸡类继承自抽象类Chicken
cock.call(); // 调用公鸡实例的叫唤方法
Hen hen = new Hen(); // 创建一个母鸡实例,母鸡类继承自抽象类Chicken
hen.call(); // 调用母鸡实例的叫唤方法
}
}
抽象类的小结: 抽象类是一个尚未完工的类,不能实例化;父类的抽象方法是由实现类(子类)来完善的。
2. 简单接口——Java 8 之前
在Java体系中,我们知道每个类最多只能继承一个父类,不能同时继承多个类,即单继承。而为了解决这个问题,就引出接口,某个类可以实现多个接口,待实现的接口名称之间以逗号分隔。
接口不从属于类,而是与类平级,类通过关键字class标识,而接口通过关键字interface标识。
接口的注意点:
- 凡是类都有构造方法,即便是抽象类也支持定义构造方法,但接口不允许定义构造方法,因为接口只用于_声明某些行为动作_,本身并非一个实体。
- 在Java8前,接口内部的所有方法都必须是抽象方法,具体的方法内部代码有赖于该接口的实现类来补充,abstract可加可不加。
- 至于接口内部的属性,默认为终态属性,即添加final前缀的成员属性(可加可不加)
依据上面的注意点,我们就用代码实现动物类。
//定义一个接口。接口主要声明一些特定的行为方法
public interface Behavior {
// 注意,接口内部的方法默认为抽象方法,所以不必添加abstract前缀
// abstract public void fly(); // 这里的abstract可加可不加
public void fly(); // 声明了一个抽象的飞翔方法
public void swim(); // 声明了一个抽象的游泳方法
public void run(); // 声明了一个抽象的奔跑方法
// 接口内部的属性默认都是终态属性,所以不必添加final前缀
public String TAG = "动物世界";
// public final String TAG = "动物世界"; // 这里的final可加可不加
// 接口不允许定义构造方法。在Java8以前,接口内部的所有方法都必须是抽象方法
}
接着定义一个鹅类,它不但继承自Bird鸟类,而且还实现了接口类。
注意:子类继承父类的格式:extends 父类名 ;实现某个接口的格式:implements 接口名 。
//定义一个实现了接口Behavior的鹅类。注意鹅类需要实现Behavior接口的所有抽象方法
public class Goose extends Bird implements Behavior {
public Goose(String name, int sexType) {
super(name, sexType);
}
// 实现了接口的fly方法
@Override
public void fly() {
System.out.println("鹅飞不高,也飞不远。");
}
// 实现了接口的swim方法
@Override
public void swim() {
System.out.println("鹅,鹅,鹅,曲项向天歌。白毛浮绿水,红掌拨清波。");
}
// 实现了接口的run方法
@Override
public void run() {
System.out.println("槛外萧声轻荡漾,沙间鹅步满蹒跚。");
}
}
对于外部调用,跟一般类没什么区别。
// 演示简单接口的实现类用法
private static void testSimple() {
Goose goose = new Goose("家鹅", 0); // 创建一个家鹅实例
goose.fly(); // 实现了接口的fly方法
goose.swim(); // 实现了接口的swim方法
goose.run(); // 实现了接口的run方法
}
3. 简单接口——Java 8 之后
在Java8之前,接口内部的所有方法都必须是抽象方法,而Java8之后,接口的内部方法也可能不是抽象方法吗?
鉴于此,从Java8开始,接口补充了以下规定:
- 增加了默认方法,并通过前缀default来标识。接口内部需要编写默认方法的完整实现代码,这样实现类无须重写该方法即可直接继承并使用,仿佛默认方法就是父类方法一样,唯一的区别在于实现类不允许重写默认方法。(即:默认方法不支持重写,但可以继承)
- 增加了静态属性和静态方法,而且通过static来标识。接口的静态属性同时也是终态属性,初始化赋值之后便无法再次修改。接口的静态方法不能被实现类继承,因而实现类允许定义同名的静态方法,缘于接口的静态方法与实现类的静态方法没有任何关联,仅仅是它们两个恰好同名而已。(即:接口内部的静态属性也默认为终态属性;静态方法支持重写,但不能被继承)
//定义一个增加了Java8新特性的接口
public interface ExpandBehavior {
public void fly(); // 声明了一个抽象的飞翔方法
public void swim(); // 声明了一个抽象的游泳方法
public void run(); // 声明了一个抽象的奔跑方法
// 默认方法,以前缀default标识。默认方法不支持重写,但可以被继承。
public default String getOrigin(String place, String name, String ancestor) {
return String.format("%s%s的祖先是%s。", place, name, ancestor);
}
// 接口内部的静态属性也默认为终态属性,所以final前缀可加可不加
public static int MALE = 0; // 雄性
public static int FEMALE = 1; // 雌性
// public final static int MALE = 0;
// public final static int FEMALE = 1;
// 静态方法,以关键字static标识。静态方法支持重写,但不能被继承。
public static String getNameByLeg(int leg_count) {
if (leg_count == 2) {
return "二足动物";
} else if (leg_count == 4) {
return "四足动物";
} else if (leg_count >= 6) {
return "多足动物";
} else {
return "奇异动物";
}
}
}
//定义实现了扩展接口的鹅类
public class ExpandGoose extends Bird implements ExpandBehavior {
public ExpandGoose(String name, int sexType) {
super(name, sexType);
}
// 实现了接口的fly方法
@Override
public void fly() {
System.out.println("鹅飞不高,也飞不远。");
}
// 实现了接口的swim方法
@Override
public void swim() {
System.out.println("鹅,鹅,鹅,曲项向天歌。白毛浮绿水,红掌拨清波。");
}
// 实现了接口的run方法
@Override
public void run() {
System.out.println("槛外萧声轻荡漾,沙间鹅步满蹒跚。");
}
// 根据产地和祖先拼接并打印该动物的描述文字
public void show(String place, String ancestor) {
// getOrigin是来自扩展接口ExpandBehavior的默认方法,可以在实现类中直接使用
String desc = getOrigin(place, getName(), ancestor);
System.out.println(desc);
}
}
接着外部调用,注意以下两点:
- 接口的静态属性:实现类的名称 . 静态属性名;
- 接口的静态方法:扩展接口的名称 . 静态方法名(***)。
// 演示扩展接口的实现类用法
private static void testExpand() {
// 实现类可以继承接口的静态属性
ExpandGoose goose = new ExpandGoose("鹅", ExpandGoose.FEMALE);
// goose.fly();
// goose.swim();
// goose.run();
goose.show("中国", "鸿雁");
goose.show("欧洲", "灰雁");
// 接口中的静态方法没有被实现类所继承,因而只能通过扩展接口自身访问
String typeName = ExpandBehavior.getNameByLeg(2);
System.out.println("鹅是" + typeName);
}
4. 匿名内部类
众所周知,前面所写的接口都是自己定义的,其实Java自身也带有接口,接下来说明接口的几种调用方式。
不知大家是否有印象之前所讲着 Java之数组工具Arrays 中sort方法,该方法是默认为升序的,那时我考虑过如何将其改变为降序呢?
我便查了相关的API文档,发现有一个sort方法允许在第二个参数中传入比较器对象,该比较器就是系统自带的接口Comparator,对应的唯一抽象方法是compare,该方法的两个输入参数都是两个待比较的元素,返回-1则前者大于后者,返回1则前者小于后者。因此,我只需要自己新写一个比较器就能按照降序排列了。
最原始升序排序:
// 演示数组工具的默认升序排列
private static void sortInArrayAsx() {
Integer[] array = {
83,86,95,2,4,99,55};
Arrays.sort(array);// Arrays的sort方法默认为升序
String scr = "array的升序结果:";
for(Integer array_1 : array) {
// 拼接排序后的数组元素
scr = scr + array_1 + ",";
}
System.out.println(scr);
}
接着就自己定义一个数组比较器,
//定义一个整型数组的降序比较器
public class SortDescend implements Comparator<Integer> {
@Override
public int compare(Integer o1,Integer o2) {
//return Integer.compare(o1, o2);// 默认的参数顺序是升序
return Integer.compare(o2,o1);// 倒过来的参数顺序变成了降序
}
}
然后使用sort方法中输入两个参数,
// 利用新定义的降序比较器实现对数组的降序排列
private static void sortIntArrayDesc() {
Integer[] intArray = {
89, 3, 67, 12, 45 };
// sort方法支持按照指定的排序器进行排列判断,新定义的SortDescend类实现了降序排列
Arrays.sort(intArray, new SortDescend());
String descDesc = "整型数组的降序结果为:";
for (Integer item : intArray) {
// 拼接排序后的数组元素
descDesc = descDesc + item + ", ";
}
System.out.println(descDesc);
}
从上面代码可以看出,固然实现了降序功能,但是存在以下缺点:
- 简简单单的几行compare代码,还得专门用一个代码文件来存储;
- 即使不开辟,也得在源代码增加内部类,也会把排序方法与比较器隔开一定距离,尽管说距离产生美,但距离也会产生隔阂呀!
- …
为了解决上面问题,我们将采取最便捷的方案处理。为此,Java创造一种名叫“匿名内部类”的概念,这个本质上属于内部类。
优点:匿名内部类的方法定义与实例创建操作合二为一,代码看起来更加流利(即不需要另外定义一个继承接口的实现类)。
“匿名内部类”的实例创建格式:
new 接口名称(){
//这里要实现该接口声明的抽象方法
}
以上两个重要信息:
- new表示创建实例对象;
- 接口名称表示该对象实现了指定接口。
匿名内部类无需专门定义形态完整的类,只需指明新创建的实例从哪个接口扩展而来。
接下来就把它应用到数组降序优化环节,
// 通过匿名内部类完成自定义的排序操作
private static void sortInArrayDescAnonymous() {
Integer[] array = {
83,86,95,2,4,99,55};
// 匿名内部类无需专门定义形态完整的类,只需指明新创建的实例从哪个接口扩展而来
Arrays.sort(array, new Comparator<Integer>() {
public int compare(Integer o1, Integer o2) {
return Integer.compare(o2, o1);
}
});
String scr = "array采用匿名内部类的降序结果:";
for(Integer array_1 : array) {
scr = scr + array_1 + " ";
}
System.out.println(scr);
}
数组比较器不仅适用于整型数组,还适用于其他类型。再举个例子,利用Arrays工具的sort方法给字符串数组长度进行排序。
// 通过匿名内部类对字符串数组按照字符串长度排序
private static void sortStrArrayByLength() {
String[] strArray = {
"说曹操曹操就到", "东道主", "风马牛不相及", "亡羊补牢", "无巧不成书",
"冰冻三尺非一日之寒", "同窗", "青出于蓝而胜于蓝" };
// 字符串数组的默认排序方式为根据首字母的拼写顺序,
// 下面的匿名内部类把排序方式改成了按照字符串长度排序
Arrays.sort(strArray, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.length() > o2.length() ? -1 : 1; // 比较前后两个数组元素的字符串长度大小
}
});
String desc = "字符串数组比较字符串长度的升序结果为:";
for (String item : strArray) {
// 拼接排序后的数组元素
desc = desc + item + ",";
}
System.out.println(desc);
}