在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的, 如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。
比如,动物类都有自己的name,有age,都会eat,但是根据这些成员我们能判断出动物是什么吗?
是一条狗?一只猫?还是一条鱼?又或者四不像?
我们先给动物类定义一些属性和行为
class Animal {
//定义成员变量
String name;
int age;
//定义吃饭行为
public void eat() {
System. out.println(this.name+"在吃饭!");
}
}
再来定义继承动物的狗类和猫类
class Dog extends Animal {
public void eat() {
System. out.println(this.name+"在啃骨头!");
}
}
class Cat extends Animal {
public void eat() {
System. out.println(this.name+"在摸鱼!");
}
}
观察上面的代码就会发现,猫类和狗类都把eat方法重写了,说明Animal类里面的eat方法不需要具体的实现代码,可以改成这样
class Animal {
String name;
int age;
public void eat() {
}
}
为了让代码更简洁,可不可以不要'{}',而是直接在这个方法的后面加上' ; '呢
实际上是可以的,但是需要借助abstract关键字,表示这是一个抽象方法
public abstract void eat() ;
像这种没有实际工作的方法, 我们可以把它设计成一个 抽象方法(abstract method), 包含抽象方法的类我们称为 抽象类
下面的内容会很多,各位看官一定要耐心!
1.抽象类和抽象方法前必须要有abstract修饰
2.抽象类中可以有普通成员变量或方法
3.抽象类可以没有抽象方法,有抽象方法的类一定是抽象类
4.抽象类不能实例化
5.如果一个普通类继承了抽象类,这个普通类就要重写抽象类中的所有未被重写的抽象方法
6.如果一个类不想全部重写抽象类的抽象方法,这个类也要被定义成抽象类
如果觉得有点绕,不防看看下图图解
先是定义了一个抽象类Animal,类体里面有抽象方法eat和sleep
定义了Fish类来继承Animal,但是这个类只是重写了sleep方法,所以Fish也必须是抽象类
定义了GoldFish类继承鱼类,如果想让GoldFish实例化出一个金鱼,就必须把它定义成普通类,所以它必须重写未被重写的抽象方法eat
总而言之,所有被普通类继承的抽象方法至少要被重写一次
你以为这就完了?其实还有两条规则
7.抽象类方法要满足被重写的要求(不能被private,static,final修饰,不能是构造方法)
8.抽象类中可以存在构造方法,在子类实例化时帮助父类成员初始化
为了便于更好的理解上面的规则,我们上代码看一下
拿我们的固定嘉宾Animal举例吧
下面是Animal类的定义
abstract class Animal {
private String name;
private int age;
//定义Animal类的构造方法
Animal(String name,int age) {
this.name=name;
this.age=age;
}
//定义抽象方法eat和play
public abstract void eat();
public abstract void play();
}
接下来定义一直陪伴我们的朋友---
class Dog extends Animal {
Dog(String name,int age) {
super(name,age);
}
@Override
public void eat() {
System. out.println("汪汪汪~要啃骨头");
}
@Override
public void play() {
System. out.println("汪汪汪~在拿耗子");
}
}
class Cat extends Animal {
Cat(String name, int age) {
super(name, age);
}
@Override
public void eat() {
System. out.println("喵喵喵~在吃鱼");
}
@Override
public void play() {
System. out.println("喵喵喵~在玩毛球");
}
}
结合前面学过的继承和多态,来看看下面的test代码会运行出什么结果吧
public class Test {
public static void func(Animal animal) {
animal.eat();
animal.play();
}
public static void main(String[] args) {
func(new Dog("小白",2));
System. out.println("************************");
func(new Cat("小黑",1));
}
}
运行结果
看到这里,是不是还有疑问?既然普通类可以被继承,也能被重写,为啥非要把Animal定义成抽象类捏?不急,接着往下翻
因为抽象类并不能完整地描述一个实体,对比普通类,抽象类本身不能被实例化,只能创建子类,让子类实例化出对象。可以说抽象类存在的最大意义就是被继承。
所以,定义一个抽象类相当于多了一层编译器的校验。
很多功能不应该由父类去完成,而是应该让子类自己定义,在这种情况下,如果我们不小心new了一个抽象父类,编译器不会袖手旁观。普通父类并没有这个特权。
很多语法存在的意义都是为了 "预防出错", 例如我们曾经用过的 final 也是类似. 创建的变量不允许用户去修改,但是加上 final 能够在不小心误修改的时候, 让编译器及时提醒我们.
在现实生活中,接口的例子比比皆是,比如下面这个插排
如果你拿着一个两头的充电器,就不能使用这个插排
所以接口就是公共的行为规范标准,只要在实现时符合规范标准,就可以通用
在Java中,接口可以看成是多个类的公共规范
把class换成interface,就可以定义一个接口
public interface 接口名称{
抽象方法
}
接口的定义一般有以下规则(不强制)
1.接口名称前面一般加上'I'修饰
2.接口中的属性和方法一般不加修饰符,保持代码简洁性
规则也有很多,请冲一杯咖啡慢慢理解
1.接口内的方法都是抽象方法,除了被defult修饰的方法(JDK1.8后引入的)和静态方法
2.接口内的方法默认被public abstract修饰
3.接口内的成员变量都默认被public static final修饰,在定义时要初始化
4.接口不能new出对象
5.接口不能有代码块和构造方法
比如,定义一个插排的充电接口
public interface IChargeUSB {
int capacity=10;//定义一个成员表示容量
private String color="red";//error,不允许有除了public,static,final的其他修饰符
static {
capacity=100;
}//error,接口内部不允许有代码块
//定义一个可以实现的方法
//方法前面不允许添加除了public,abstract以外的修饰符
default void open() {
System. out.println("已经连通,可以充电了!");
}
//定义一个抽象方法
void close();
//定义一个静态方法获取容量
static int getCapacity() {
return capacity;
}
}
既然有了充电接口这个标准,想要使用它就必须与类联系到一起,看看语法是怎么规定的吧
6.一个类想要实现一个接口,需要用implements关联
7.普通类实现了一个接口,就必须重写接口内所有的抽象方法
8.接口内被default修饰的方法可重写可不重写,被static修饰的方法不能被重写
9.如果实现接口的类不想全部重写接口的抽象方法,就必须定义成抽象类
来定义你手上的手机或者面前的电脑实现这个接口吧
public class Phone implements IChargeUSB{
@Override
public void open() {
System. out.println("您的手机已充上电!");
}//被default修饰的方法,可重写可不重写
@Override
public void close() {
System. out.println("您的手机已断开充电线!");
}//抽象方法,必须重写
}
下面来定义电脑
public class Computer implements IChargeUSB{
@Override
public void close() {
System. out.println("您的电脑已断开电源");
}
}
接口和抽象类一样,都可以通过引用子类对象向上转型,从而实现多态,来体会一下吧
public class Test {
public static void func(IChargeUSB usb) {
usb.open();
usb.close();
}
public static void main(String[] args) {
System. out.println("插排容量为:"+IChargeUSB. getCapacity());
func(new Phone());
System. out.println("*********************");
func(new Computer());
}
}
输出结果
别看接口不能和类一样实例化,每个接口也都有自己对应的字节码文件
建议每个java文件里只放一个接口或者一个类
在继承和多态里面提到,Java中是没有多继承的,但是现实生活中一个类仅仅继承一个父类是不够的
比如Animal类,只能给它定义全部动物的共同属性---name,age,只能给它定义全部动物的共同行为---eat,sleep
但是狗和猫除了会吃饭睡觉,还会撒娇,会捉老鼠,他们还有很多共同的功能...
实现多个接口就是为了解决多继承的问题
一个类只能继承一个基类,可以实现多个接口
来拿Animal举例
abstract public class Animal {
//定义成员变量
private String name;
private int age;
//定义抽象方法
public abstract void eat();
public abstract void sleep();
//定义构造方法
Animal(String name,int age) {
this.name=name;
this.age=age;
}
//定义public方法获取成员变量
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
然后我们再定义狗类和猫类的共同功能
//解锁捉耗子功能
public interface ICatch {
void catchMouse();
}
//解锁玩耍功能
public interface IPlay {
void play();
}
这样就可以完善狗和猫了
注意:必须先继承类才能实现接口,也就是说extends必须放在implements的前面
//狗类继承了Animal抽象类,并且实现了ICatch和IPlay接口
public class Dog extends Animal implements ICatch,IPlay{
//帮助完成父类成员初始化
Dog(String name, int age) {
super(name, age);
}
@Override
public void eat() {
System. out.println(getName()+"正在吃狗粮");
}
@Override
public void sleep() {
System. out.println("汪汪汪~"+getName()+"在睡觉");
}
@Override
public void catchMouse() {
System. out.println("汪汪汪~"+getName()+"在拿耗子");
}
@Override
public void play() {
System. out.println("汪汪汪~"+getName()+"在接飞盘");
}
}
public class Cat extends Animal implements ICatch,IPlay{
//帮助完成父类成员初始化
Cat(String name, int age) {
super(name, age);
}
@Override
public void eat() {
System. out.println(getName()+"在吃鱼");
}
@Override
public void sleep() {
System. out.println("喵喵喵~"+getName()+"在晒太阳睡大觉");
}
@Override
public void catchMouse() {
System. out.println("喵喵喵~"+getName()+"在看旺财拿耗子");
}
@Override
public void play() {
System. out.println("喵喵喵~"+getName()+"在玩毛球");
}
}
如果我们想要实现动物的功能或者获取动物的属性,可以接收Animal类的引用,如果想要实现ICatch和IPlay的功能,可以接收ICatch和IPlay的引用
public static void funcAnimal(Animal animal) {
System. out.println("Hello,I'm"+animal.getName()+"我今年"+animal.getAge()+"岁了");
animal.eat();
animal.sleep();
}
public static void funcICatch(ICatch icatch) {
icatch.catchMouse();
}
public static void funcIPlay(IPlay iplay) {
iplay.play();
}
现在到了收获结果的时候了
public static void main(String[] args) {
Dog dog=new Dog("旺财",2);
funcAnimal(dog);
Cat cat=new Cat("小白",1);
funcICatch(cat);
funcIPlay(cat);
}
运行结果
不论是抽象类还是接口,都可以通过向上转型实现多态
我猜你肯定会问到:
为啥还要多增添两个接口?直接把它们的功能给Animal类然后继承不就完事嘛!
答曰:不是所有的动物都会多管闲事去拿耗子,博主就对耗子不感兴趣
使用接口可以让我们忽略类,只要某个对象有这个接口,它就可以实现这个接口的方法
比如再定义一个模型火车
public class Toy implements IPlay{
@Override
public void play() {
System. out.println("呜呜~我会沿着轨道跑");
}
}
这时候只要调用funcIPlay方法,这个火车也可以用来逗隔壁邻居家的傻儿子玩
public static void main(String[] args) {
funcIPlay(new Toy());
}
一个接口可以继承多个接口,从而实现这个接口功能的扩展
接口之间的继承要用到关键字extends,语法规则如下
interface A extends B,C {
}
上面的代码让接口A继承了接口B和接口C,如果有某个类想要实现接口A,就要把接口B和接口C的抽象方法全部重写
你肯定又要问了
为什么不直接把testA,B,C三个功能直接给接口C?还在这里脱裤子放屁
答曰:通过这种接口间相互继承的方式,可以实现代码的低耦合。如果更改了C接口,实现A接口和B接口的类不会受到影响
下面的接口全部是针对自定义类型
先定义一个学生类
class Students {
//定义学生类成员变量,姓名,身高,成绩
String name;
int height;
double score;
//定义构造方法
public Students(String name, int height, double score) {
this.name = name;
this.height = height;
this.score = score;
}
}
现在全班有20个学生,他们班主任面前有个两难的问题,究竟是按身高还是按成绩给学生排座呢?
先不管班主任的问题,你有没有想过这些学生能排序吗?
对于类型元素是基本数据类型的数组,可以用Arrays.sort()实现排序
public static void main(String[] args) {
int[] array={2,5,9,3,0};
Arrays. sort(array);
System. out.println(Arrays. toString(array));
}
输出结果
[0, 2, 3, 5, 9]
对于一个学生数组,Arrays还能帮助排序吗?
public class Test {
public static void main(String[] args) {
Students[] st=new Students[3];
st[0]=new Students("喜羊羊",125,98);
st[1]=new Students("美羊羊",115,80);
st[2]=new Students("懒羊羊",120,60);
Arrays. sort(st);
for(Students e:st) {
System. out.println(e);
}
}
}
运行结果就是:
点开异常,就会发现sort其实是把数组元素转换成了Comparable接口,并且调用了这个接口的compareTo方法,所以只要学生类实现了这个接口并且重写了compareTo方法,就可以实现排序
所以重新定义一下学生类,让它可以按照身高排序
我们先使用这种规则重写compareTo方法
如果自己的身高大于要对比对象的身高,返回一个大于0的整形数字
//实现Comparable接口
class Students implements Comparable{
String name;
int height;
double score;
public Students(String name, int height, double score) {
this.name = name;
this.height = height;
this.score = score;
}
//重写toString方法
@Override
public String toString() {
return "Students{" +
"name='" + name + '\'' +
", height=" + height +
", score=" + score +
'}';
}
//重写compareTo方法
@Override
public int compareTo(Students o) {
return this.height-o.height;//此处如果按照成绩排序,需要将结果强转成int
//(int)(this.score-o.score)
}
}
来看一下运行结果
从运行结果也可以看出,上面的运行规则可以让学生按照身高的升序排列,也可以推断出compareTo的使用方式
1.如果当前对象应排在参数对象之前, 返回小于 0 的数字;
2.如果当前对象应排在参数对象之后, 返回大于 0 的数字;
2.如果当前对象和参数对象不分先后, 返回 0;
为了更好地理解Comparable接口,来用冒泡排序实现一下sort过程
public static void main(Comparable[] array) {
for(int i=0;i0) {
Comparable ret=array[j];
array[j]=array[j+1];
array[j+1]=ret;
}
}
}
}
前面的Comparable接口可以让我们实现排序,但也固定了学生排序的方式只能是按照身高,如果慢羊羊村长在上课的时候不想看见打鼾的懒羊羊,就要按照成绩进行排序
如果想实现一个类的多种排序方式,要用到Comparator接口
这时候就可以定义两个类分别实现compare方法
//按照身高排序
class heightComparator implements Comparator{
@Override
public int compare(Students o1, Students o2) {
return o1.height-o2.height;
}
}
//按照成绩排序
class scoreComparator implements Comparator{
@Override
public int compare(Students o1, Students o2) {
return (int)(o1.score-o2.score);
}
}
接下来分别按照身高和成绩比较美羊羊和懒羊羊
public class Test {
public static void main(String[] args) {
Students st1=new Students("懒羊羊",120,60);
Students st2=new Students("美羊羊",115,80);
heightComparator hc=new heightComparator();
//定义身高比较器
int ret=hc.compare(st1,st2);
System.out.print("按照身高排序:");
if(ret>0) {
System.out.println(st1.name+">"+st2.name);
} else if(ret<0) {
System.out.println(st1.name+"<"+st2.name);
} else System.out.println(st1.name+"="+st2.name);
//定义分数比较器
scoreComparator sc=new scoreComparator();
ret=sc.compare(st1,st2);
System.out.print("按照成绩排序:");
if(ret>0) {
System.out.println(st1.name+">"+st2.name);
} else if(ret<0) {
System.out.println(st1.name+"<"+st2.name);
} else System.out.println(st1.name+"="+st2.name);
}
}
现在有一个手机对象
public class Phone {
//手机的成员变量
String color;
String brand;
Detail dt;
//手机的构造方法
public Phone(String color, String brand, Detail video) {
this.color = color;
this.brand = brand;
this.dt = video;
}
}
class Detail {
//手机大小
int size=10;
//手机重量
int height=10;
}
这部手机是属于你妈妈的,你正好想换新手机了,觉着妈妈的手机用着不错,想要克隆一部出来
这时候就要使用clone方法
public static void main(String[] args) {
Phone mPhone=new Phone("white","huawei",new Detail());
Phone myPhone=mPhone.clone();
}
上面这串代码是不能使用的,想要使用clone方法就要先了解它的规则
可以看到,clone是Object类下的一个方法,会返回一个Object对象(Object是所有类的祖先,即使这个类没有显式定义继承了Object),这个方法由protected限制,可以被不同包的子类访问,native表示它的底层是由C/C++实现的,后面的抛异常以后再详细解释(目前我也不知道它的语法)
所以我们的目的就明确了,重写clone方法
public class Phone {
String color;
String brand;
Detail dt;
public Phone(String color, String brand, Detail video) {
this.color = color;
this.brand = brand;
this.dt = video;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Detail {
int size=10;
int height=10;
}
class test {
//仔细看main后面的内容,这是处理异常的方式
public static void main(String[] args) throws CloneNotSupportedException {
Phone mPhone=new Phone("white","huawei",new Detail());
Phone myPhone=(Phone)mPhone.clone();//返回类型是Object,如果使用Phone接收要向下转型
}
}
运行一下发现程序抛出了异常
翻译过来就是不支持克隆,解决方法:在克隆的类后面实现Cloneable接口
public class Phone implements Cloneable
实际上这个接口啥也没有实现...
Cloneable接口只是一个标志接口,表示这个类型的对象可以被克隆
所以你拥有了妈妈同款手机
public static void main(String[] args) throws CloneNotSupportedException {
Phone mPhone=new Phone("white","huawei",new Detail());
Phone myPhone=(Phone)mPhone.clone();
System. out.println("妈妈的手机:"+mPhone);
System. out.println("我的新手机:"+myPhone);//Phone和Detail类要先实现toString的重写,
//下文会给出完整代码
}
你又发现自己的idol王一博换了一个带有摩托图案的手机壳,既然买不起他的手机,就买同款手机壳吧,于是你手机的height属性变重了
myPhone.dt.height=11;
System. out.println("妈妈的手机:"+ mPhone.dt);
System. out.println("我的手机:"+myPhone.dt);
你妈妈奇怪的发现,诶,我手机咋也变沉了?
看图解
因为dt成员是个引用变量,所以它存储的是对象的地址,clone的时候只是把地址拷贝过去了,所以你的手机和妈妈的手机捆绑在一块了,这显然是不合理的
要进行深拷贝,就是把所有的对象都拷贝一遍!
//让手机里的Detail类型的成员进行拷贝,就是让Detail类重写clone方法
class Detail implements Cloneable{
int size=10;
int height=10;
@Override
public String toString() {
return "Detail{" +
"size=" + size +
", height=" + height +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
这也是不管用的,问题出在了哪里呢?
看Phone类的clone方法
它的clone方法只是调用了父类的clone,没有做其他的改变
应该改成下面这样
protected Object clone() throws CloneNotSupportedException {
Phone tmp=(Phone)super.clone();//使用Phone变量接收this对象通过父类克隆的对象
tmp.dt=(Detail)this.dt.clone();//拷贝dt并更改tmp指向的dt对象
return tmp;
}
用图来说话就是这样子的
我们再来打印输出刚才的结果
下面是完整的代码
public class Phone implements Cloneable{
String color;
String brand;
Detail dt;
public Phone(String color, String brand, Detail video) {
this.color = color;
this.brand = brand;
this.dt = video;
}
@Override
public String toString() {
return "Phone{" +
"color='" + color + '\'' +
", brand='" + brand + '\'' +
", dt=" + dt +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
Phone tmp=(Phone)super.clone();
tmp.dt=(Detail)this.dt.clone();
return tmp;
}
}
class Detail implements Cloneable{
int size=10;
int height=10;
@Override
public String toString() {
return "Detail{" +
"size=" + size +
", height=" + height +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class test {
public static void main(String[] args) throws CloneNotSupportedException {
Phone mPhone=new Phone("white","huawei",new Detail());
Phone myPhone=(Phone)mPhone.clone();
System.out.println("妈妈的手机:"+mPhone);
System.out.println("我的新手机:"+myPhone);
myPhone.dt.height=11;
System.out.println("妈妈的手机:"+ mPhone.dt);
System.out.println("我的手机:"+myPhone.dt);
}
}
Object是Java默认提供的一个类。Java里面除了Object类,所有的类都是存在继承关系的。默认会继承Object父类。
所有类的对象都可以使用Object的引用进行接收。
也就是说,如果一个类没有显式继承某个基类,它也会默认继承Object类,如果它继承了某个基类,因为这个基类也是Object的子类,所以这个基类的子类多层继承了Object类
Object类提供了很多方法供我们使用
其中的toString和clone已经讲述过,接下来看getClass,hashCode和equals方法(其余的以后会提到)
这个方法就是用来告知类的信息,也不能被重写,没有其他的功能
来看看它的使用
public static void main(String[] args) throws CloneNotSupportedException {
Phone mPhone=new Phone("white","huawei",new Detail());
Class> c=mPhone.getClass();
System. out.println(c);
}
输出结果
在Java中,如果使用==判断两个对象,实际上比较的使它们的地址
public static void main(String[] args) throws CloneNotSupportedException {
Phone mPhone=new Phone("white","huawei",new Detail());
//克隆妈妈的手机
Phone myPhone=(Phone)mPhone.clone();
System. out.println(mPhone);//此时Phone类还未定义toString方法
System. out.println(myPhone);
System. out.println(myPhone==mPhone);
}
运行结果
但是我的手机在换手机壳之前是克隆的妈妈的手机呀,想更改默认的比较逻辑,让它们相等怎么办?
这时候要在类内部重写equals方法
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Phone phone = (Phone) o;
return Objects.equals(color, phone.color) && Objects.equals(brand, phone.brand) && Objects.equals(dt, phone.dt);
}
当然了,在自定义类型Detail里也要实现equals的重写,String类有自己的equals方法,调用就管
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Detail detail = (Detail) o;
return size == detail.size && height == detail.height;
}
你以为这些方法逻辑那么强,还要判断形参是不是null类型,博主考虑地也太周到了吧(嘿嘿,虽然最后一句话是我杜撰的,这个方法也不是我写的,就自夸一下下吧)
用idea快捷生成就行了
这样我们就可以看看根据我们的逻辑,两个手机是否相同了
public static void main(String[] args) throws CloneNotSupportedException {
Phone mPhone=new Phone("white","huawei",new Detail());
Phone myPhone=(Phone)mPhone.clone();
System.out.println(myPhone.equals(mPhone));
}
实际上我们在重写toString方法的时候见过它,只是我没有提
hashCode方法会返回这个对象的地址
class test {
public static void main(String[] args) throws CloneNotSupportedException {
Phone mPhone=new Phone("white","huawei",new Detail());
Phone myPhone=(Phone)mPhone.clone();
System. out.println(mPhone.hashCode());
System. out.println(myPhone.hashCode());
}
}
运行结果
460141958
1163157884
如果我们想让这两部手机的hash值一样,也可以重写hashCode方法
像生成equals方法一样用idea生成就行了(所有类型都要生成),然后你就会得到下面的代码
Phone类
@Override
public int hashCode() {
return Objects. hash(color, brand, dt);
}
Detail类
@Override
public int hashCode() {
return Objects. hash(size, height);
}
这时候再打印两部手机的hashCode值,就会发现他们是一样的
至于为啥非得重写它们的hashCode方法
事实上hashCode() 在散列表中才有用,在其它情况下没用。在散列表中hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。
建议所有自定义类型都把equals和hashCode方法重写一遍,反正用idea就好了
下面的部分实际上是前面内容的简单总结
有些类不能完整地描绘出一个实体,就需要用到抽象类
只要有共同的功能,就可以实现同一个接口,接口可以打破类的限制
抽象类 |
接口 |
|
成员组成 |
可以有各种成员 |
只能有public static final修饰的成员变量,只能有static或default修饰的方法或抽象方法 |
成员权限 |
可以有各种权限 |
只能是public权限 |
定义方式 |
abstract class |
interface |
被继承关键字 |
extends |
implements |
被继承个数 |
一个类只能继承一个抽象类 |
一个类可以实现多个接口 |
能否实例化 |
不能 |
不能 |
总而言之,接口是比抽象类还抽象的类型...over!