文章目录
基本内容
- 抽象类
- 接口
- 多态
- Java中关于instanceof的正解
抽象类(关键字abstract)
抽象的特性:
- 抽象类和抽象方法都需要被abstract修饰
- 抽象类无法创建对象
- 抽象类中可以有构造方法,是供子类创建对象时,初始化父类成员使用
- 抽象类中,不一定包含抽象方法,但是有抽象方法的一定是抽象类
- 可以不让该类创建对象,方法可以直接让子类去使用
- 抽象类的子类,必须重写抽象父类中所有的抽象方法
//研发部员工
abstract class Developer {
public abstract void work();//抽象函数。需要abstract修饰,并分号;结束
}
//JavaEE工程师
class JavaEE extends Developer{
public void work() {
System.out.println("正在研发淘宝网站");
}
}
//Android工程师
class Android extends Developer {
public void work() {
System.out.println("正在研发淘宝手机客户端软件");
}
}
抽象方法:
使用abstract修饰的方法,没有方法体,只有声明。定义一种规范,子类必须要给抽象方法提供具体的实现。
定义格式:
修饰符 abstract 返回值类型 方法名 (参数列表);
抽象类
包含抽象方法的类就是抽象类。通过abstract方法定义规范,子类必须定义具体实现。抽象类也可以定义普通方法,但是无法new不能实例化对象(因为抽象方法没有方法体),只能被子类调用,只能被子类继承。
定义格式:
修饰符 abstract class 类名{}
abstract关键字与哪些关键字无法共存
- private:私有的方法子类是无法继承的,也不存在覆盖,而abstract和private一起使用,abstr要子类去实现方法,而private不让子类得到方法
- final无法使用
- static无法使用
接口
定义(关键字interface)
可以先理解为比“抽象类”还“抽象”的“抽象类”,可以更加规范的对子类进行约束,全面地实现了:规范和具体实现的分离。
从接口的实现着角度看,接口定义了可以向外部提供的服务。
从接口的调用角度看,接口定义了实现者能提供哪些服务。
接口是功能的集合,是一种公共的规范标准。
好处
当你不需要接口的时候,可以直接在实现类中把implements删除,那么实现类依然可以使用(前提是接口里面全是抽象类)。如果是抽象类的话,删除抽象类,子类继承完全没法用了(因为抽象类并不一定写的是抽象方法)。
定义:
修饰符 interface 接口名
[
extends父接口
1
,父接口2
…]{ }
//定义一个Volent接口
interface Volent{
//定义一个常量,由于默认使用public static final类型
int FLY_HIGHT;
//定义一个抽象方法,默认public abstract void fly ();
void fly();
}
说明:
-
- 访问修饰符:只能是public或默认。
- 接口名:和类名采用相同命名机制。
- extends:接口可以多继承,在没有继承的时候[]不写。
- 常量:接口中的属性只能是常量,总是:public static final 修饰。不写也是。
- 方法:接口中的方法只能是:public abstract。 省略的话,也是public abstract。
接口中包含的内容(分版本):
-
JDK7
-
常量
抽象方法(最常用)
-
-
JDK8
-
默认方法,default
静态方法,static
-
-
JDK9(了解)
-
私有方法,private
-
使用(关键字implements)
实现类类通过implements来实现接口中的规范
//定义一个实现类实现上方的飞行
class Angel implements Volant{
public void fly(){
System.out.println("我是天使,飞起来啦!");
}
}
默认方法
在JDK8以及以后的版本,接口支持定义默认方法
优势:
- 为了在修改接口的时候不用在实现类中全部的修改
定义:
修饰符 default 返回值类型 方法名 (参数列表){
方法体
}
public default void run (){
System.out.println("这是默认方法");
}
注意事项:
- 接口中使用default修饰的方法,需要有方法体
- 接口中使用default修饰的方法,实现类可以直接使用这个方法
- 实现类可以重写default修饰的方法,也可以不重写
- default方法在接口中,可以省略前面的修饰符
静态方法
在JDK8以及以后的版本,接口支持定义静态方法。
定义:
修饰符 static 返回值类型 方法名(参数列表){
方法体
}
public interface Demo{
public static void run(){
System.out.println("这是静态方法");
}
}
注意事项:
-
接口中使用static修饰的方法,需要有方法体
-
接口中使用static修饰的方法,实现类无法继承到接口中的静态方法
-
静态方法的调用:接口名.静态方法名(参数列表)
Demo.run();
-
static方法在接口中,可以省略前面的修饰符,但是为了可以正确的区分这是静态方法一定要留好
public static
对比记忆:
- 类与类之间的继承关系,子类可以继承父类的静态方法(变量)
- 接口和实现类之间的关系,实现类是无法继承接口中的静态方法
多继承(多实现)
实现类多实现
public class InterFaceAB implements InterFaceA,InterFaceB{
}
多实现中的方法重名问题:
- 抽象方法的重名问题:
- 不影响实现类,因为抽象方法的实现方式为实现类的重写。
- 但是说返回值类型必须一致,否则编译错误,会发生冲突,使用了不兼容的返回类型
- default方法的重名问题:
- 要求必须在实现类将default重写,并且在实现类将default删除,原因是default只能在接口中定义。也就是说重写default定义的方法,但是不写default
- static方法的重名问题:
- 并不会影响实现类,因为实现类无法继承接口中的静态方法,只能在测试中使用:接口名.静态方法名();
接口多继承
interface Face extends FaceA,FaceB{
}
多态
多态的实际应用是在参数的传递上。
父类作为参数传递的话,实际的传值,可以传任意子类对象。
多态的要点
-
多态是方法的多态,不是属性的多态(多态与属性无关)。
-
多态的存在要有3个必要条件:继承(接口的实现),方法重写,父类引用指向子类对象。
-
父类引用指向子类对象后,用该父类引用调用子类重写的方法,此时多态就出现了。
定义方式:
//父类的引用,指向子类的对象
// 父类 变量名 = new 子类();
Fu fu = new Zi();
一个父类的引用可以指向多个子类的对象
多态中的成员(多态是方法的多态,不是属性的多态):
- 成员变量:
- 当你的父类中没有定义成员,在多态中,调用该成员,就会失败。
- 成员变量的编译看父类(也就是=的左边),父类没有编译失败
- 成员方法:
- 子类重写父类方法,运行子类的方法,否则运行父类的方法
- 编译也是看父类(也就是=的左边),父类没有方法,编译失败
向上转型
- 声明父类却实例化一个子类,这种情况为向上转型
- 在编译时为父类,在运行时是子类
- 实例可以访问父类的成员变量,但是不能访问子类的成员变量
- 实例可以访问父类的方法,但不能访问子类新增的方法。也就是说只有子类进行重写父类方法的时候,调用的是子类的方法
父类类型 变量名 = new 子类类型();
如:Person p = new Student();
向下转型
- 先声明一个父类变量然后实例化创建一个子类的实例。下面使用强制类型转换转换为子类的引用。
子类类型 变量名 = (子类类型) 父类类型的变量;
//变量p 实际上指向Student对象
如:Student stu = (Student) p;
多态的好处与弊端
当父类的引用指向子类对象时,就发生了向上转型,即把子类类型对象转成了父类类型。向上转型的好处是隐藏了子类类型,提高了代码的扩展性。
但向上转型也有弊端,只能使用父类共性的内容,而无法使用子类特有功能,功能有限制。
//描述动物类,并抽取共性eat方法
abstract class Animal {
abstract void eat();
}
// 描述狗类,继承动物类,重写eat方法,增加lookHome方法
class Dog extends Animal {
void eat() {
System.out.println("啃骨头");
}
void lookHome() {
System.out.println("看家");
}
}
// 描述猫类,继承动物类,重写eat方法,增加catchMouse方法
class Cat extends Animal {
void eat() {
System.out.println("吃鱼");
}
void catchMouse() {
System.out.println("抓老鼠");
}
}
public class Test {
public static void main(String[] args) {
Animal a = new Dog(); //多态形式,创建一个狗对象
a.eat(); // 调用对象中的方法,会执行狗类中的eat方法
// a.lookHome();//使用Dog类特有的方法,需要向下转型,不能直接使用
// 为了使用狗类的lookHome方法,需要向下转型
// 向下转型过程中,可能会发生类型转换的错误,即ClassCastException异常
// 那么,在转之前需要做健壮性判断
if( !(a instanceof Dog){ // 判断当前对象是否是Dog类型
System.out.println("类型不匹配,不能转换");
return;
}
Dog d = (Dog) a; //向下转型
d.lookHome();//调用狗类的lookHome方法
}
}
/*
描述毕老师和毕姥爷,
毕老师拥有讲课和看电影功能
毕姥爷拥有讲课和钓鱼功能
*/
class 毕姥爷 {
void 讲课() {
System.out.println("政治");
}
void 钓鱼() {
System.out.println("钓鱼");
}
}
// 毕老师继承了毕姥爷,就有拥有了毕姥爷的讲课和钓鱼的功能,
// 但毕老师和毕姥爷的讲课内容不一样,因此毕老师要覆盖毕姥爷的讲课功能
class 毕老师 extends 毕姥爷 {
void 讲课() {
System.out.println("Java");
}
void 看电影() {
System.out.println("看电影");
}
}
public class Test {
public static void main(String[] args) {
// 多态形式
毕姥爷 a = new 毕老师(); // 向上转型
a.讲课(); // 这里表象是毕姥爷,其实真正讲课的仍然是毕老师,因此调用的也是毕老师的讲课功能
a.钓鱼(); // 这里表象是毕姥爷,但对象其实是毕老师,而毕老师继承了毕姥爷,即毕老师也具有钓鱼功能
// 当要调用毕老师特有的看电影功能时,就必须进行类型转换
毕老师 b = (毕老师) a; // 向下转型
b.看电影();
}
}
总结
什么时候使用向上转型
当不需要面对子类类型时,通过提高扩展性,或者使用父类的功能就能完成相应的操作,这时就可以使用向上转型。
如:Animal a = new Dog();
a.eat();
什么时候使用向下转型
当要使用子类特有功能时,就需要使用向下转型。
如:Dog d = (Dog) a; //向下转型
d.lookHome();//调用狗类的lookHome方法
向下转型可能出现的问题
-
向下转型的好处:可以使用子类特有功能。
-
弊端是:需要面对具体的子类对象;在向下转型时容易发生ClassCastException类型转换异常。在转换之前必须做类型判断。
如: if( !(a instanceof Dog){…}
instanceof运算符
说明:
instanceof是二元运算符,类似于==,>,<等操作符。
如果有obj instanceof T,会先将obj按照T类型进行强转,如果不抛出ClassCastException 异常则返回 true ,否则值为 false 。
查找对象是否是类所创建的对象
左边是对象,右边是类;当对象是右边类或子类所创建的实例时,返回true,否则返回false
java中关于instanceof 的正确解析
作者:RednaxelaFX
链接:https://www.zhihu.com/question/46149267/answer/100442047
来源:知乎
Java里,引用的类型是静态确定的——源码里声明是什么类型就是什么类型;而对象的实际类型是动态确定的。一个静态类型为基类的引用,可以指向一个实际类型为其子类的对象实例。
多态,没错,Java的面向对象的继承多态就是体现在一个静态类型为基类的引用可以引用实际类型为子类的对象实例,并且通过这个基类引用调用虚方法可以调用到子类的实现。
如果有这样的代码:
Object obj = new Object();
此时赋值符号左手边的 obj 变量是一个引用,它的静态类型是Object;经过赋值后,它指向的是赋值符号右手边的 new 表达式所创建出来的对象,该对象的实际类型正巧也是Object。
——引用的静态类型与其在运行时指向的对象的实际类型完全一致,这个情况很好理解。
但如果变成:
Object obj = new Foo();
obj 变量仍然是一个静态类型为Object的引用,但在赋值后它所指向的对象的实际类型则不是Object了,而是Object的子类Foo。
于是回到 instanceof 运算符:它的运行时语义并不关心它的左操作数的引用的静态类型,而只关心它所引用的对象的实际类型是否跟右操作数所指定的类型相匹配。
以上面的Foo例子为例,
Object obj = new Foo();
boolean b = obj instanceof Foo;
虽然这里instanceof表达式的左操作数是一个静态类型为Object的引用,但它指向的对象的实际类型是Foo,所以该instanceof表达式的值就为true。
关于instanceof运算符的语义和实现,可以参考我的另一个回答:Java instanceof 关键字是如何实现的? - RednaxelaFX 的回答
=====================================
为啥Java里有instanceof运算符,还要提供
java.lang.Class.isInstance(Object)
方法?
其实这两者的运行时语义还真就是一样的,只不过:
- 前者是Java语言的语法结构,而该语法要求instanceof运算符的右操作数是一个引用类型名,也就意味着右操作数必须是一个(javac)编译时的常量;
- 后者是Java标准库里的一个方法,其被调用对象(receiver object,也就是“this”)等价于instanceof运算符的右操作数的意义,但可以是运行时决定的任意java.lang.Class对象,而不要求是编译时常量。这就比instanceof运算符要灵活一些。
也就是说,Class.isInstance()方法可以写出这样的代码:
public static boolean areTypesCompatible(Object expected, Object obj) {
return Objects.requireNonNull(expected)
.getClass()
.isInstance(obj);
}
来检查obj所引用的对象的实际类型是否为expected所引用的对象的实际类型的子类。显然我们不能用instanceof来实现这个功能:
return obj instanceof expected.getClass(); // doesn't compile
既然当要检查的条件(instanceof运算符的右操作数 / Class.isInstance()的被调用对象)是常量时,两者的语义一样,能不能把Class.isInstance()当作instanceof()来优化呢?
答案是肯定的。许多JVM的JIT编译器都会做这样的优化。所以经过JIT编译后,下面的两个版本的代码的效果就会是一样的:
boolean b1 = obj instanceof Foo;
boolean b2 = Foo.class.isInstance(obj);