继封装和继承之后,抽象类 、接口 和多态 同样是面向对象编程的重点。
抽象类
抽象类 :不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类 。由于抽象类不能实例化对象,所以抽象类必须被继承 ,才能被使用。类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。
抽象方法 :抽象类和接口中具有的特殊方法,与一般方法相比,抽象方法只需要声明而不需要实现,但必须在子类中实现。static方法不能声明为抽象方法
使用方法 :使用修饰符abstract声明一个抽象类, 一般具有抽象方法:abstract <类型> <方法名>(参数);
使用目的 :用于继承之后定义子类,必然会用于定义子类
特性 :1. 不能创建 对象2. 具有某个功能(抽象方法),但必须 在子类中实现。子类继承抽象类之后,必须实现抽象方法,不然仍然为抽象类
关系 (与子类):is-a ,和常规继承一样
NewsReader抽象类代码实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
public abstract class NewsReader { // 和普通类一样,可以有成员变量 private Integer x; // 和普通类一样,可以有一般方法 public Integer getX() { return x; } public void setX(Integer x) { this.x = x; } // 可以有构造方法 public NewsReader(){ System.out.println("NewsReader"); } // 抽象方法 // 具有这个功能,但具体的功能实现,在子类中确定 public abstract void readNews(); }
继承抽象类的UrlNewsReader子类代码实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14
public class UrlNewsReader extends NewsReader{ public UrlNewsReader(){ // 先调用抽象类父类构造方法 super(); System.out.println("UrlNewsReader"); } // 必须实现的抽象方法 public void readNews(){ System.out.println("Url reading......"); } }
测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13
public class Main { public static void main(String[] args) { // 尝试创建NewsReader对象 // NewsReader newsReader = new NewsReader(); // error:'NewsReader' is abstract; cannot be instantiated // 创建UrlNewsReader对象 UrlNewsReader urlNewsReader = new UrlNewsReader(); urlNewsReader.readNews(); } }
测试结果
1 2 3
NewsReader UrlNewsReader Url reading......
总结 :抽象类虽然不能创建对象,但可以有构造方法,并可以在子类中用super 关键字调用
接口
接口 :接口 是一种抽象类型,是一系列抽象方法的集合,语义上表示某种功能(比如听说读写,但没有具体的实现)。一个类通过实现接口的方式,从而来实现接口的抽象方法。接口中的抽象方法是不能在接口中实现的,只能由实现接口的类来实现接口中的方法。
意义 :把接口 和实现 真正分开
使用方法 :用关键字interface 声明接口,实现的类通过关键字implements 表达实现一个接口,从而声明这个类具有接口定义的属性和行为
特性 :1. 接口是隐式抽象 的,当声明一个接口的时候,不必使用abstract 关键字2. 接口中每一个方法 也是隐式抽象 的,声明时同样不需要abstract 关键字3. 接口中的方法都是公有的,声明不需要public 关键字(隐式的public abstract ,其它修饰符会报错)
与类的区别 :1. 接口不能用于实例化对象2. 接口没有构造方法 3. 接口中所有的方法必须是抽象方法 (没有实现)4. 接口不能包含成员变量,除了隐式的public static final 变量(其它修饰符都会报错),因为它不是类,不能实例化对象,所以不会有状态,只有属性5. 接口不是被类继承了,而是要被类实现6. 接口支持多实现,表示实现了多种功能
Displayable接口代码实例
1 2 3 4 5 6 7 8
public interface Displayable { // 隐式public static final变量 int a = 1; // 隐式public abstract方法 void display(); }
实现接口的News类代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
// News实现Displayable public class News implements Displayable { protected String title; protected String content; public News(){} // 构造的自由和责任交给用户 public News(String title, String content) { this.title = title; this.content = content; } public String getTitle() { return title; } public String getContent() { return content; } //注解表示方法来自上一层,可以检查方法名和方法类型是否写错了 @Override // 必须实现display(),且只能公有 public void display() { System.out.println(title + "n" + content); } }
测试代码
1 2 3 4 5 6 7
public class Main { public static void main(String[] args) { News news = new News("abc","def"); news.display(); } }
测试结果
问题 :抽象类和接口的选择?回答 :语义上抽象类表示一类事物的特性,而接口表示类实现的功能,除了对两者特性的区分外,对抽象类和接口的选择,得先从预估的子类抽象出来的事物来看待。如果抽象出来的共同特征能归为一个类别,用抽象类合适,如果抽象出来的特征能归为一种功能,用接口合适。因此,实现同样的功能,写法不一样,表达的意思是不一样的。例子 :Content这样的抽象,更像是一种功能,而不是一类实物的抽象,用接口较好。
多态
多态 :同一个行为具有多个不同表现形式或形态的能力
优点 :1. 消除类型间的耦合关系2. 可替换性3. 可扩充性4. 接口性5. 灵活性6. 简化性
必要条件 :1. 继承2. 重写3. 父类引用指向子类对象
实现方式 :1. 重写2. 接口(接口可以看作一种被继承才能实现的父类)3. 抽象类和抽象方法(抽象类必须作为父类来继承得以实现)
实例1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
class Animal{ public void move(){ System.out.println("Animals can move"); } } // 继承 class Dog extends Animal{ // 重写 public void move(){ System.out.println("Dogs can walk and run"); } public void bark(){ System.out.println("Dogs can bark"); } } public class Test{ public static void main(String args[]){ Animal a = new Animal(); // 父类引用指向父类对象 Animal b = new Dog(); // 父类引用指向子类对象 a.move(); b.move(); // Test.java:30: cannot find symbol symbol : method bark() location: class Animal b.bark();^ // Animal类没有bark()方法 // b.bark(); } } // 结果 Animals can move Dogs can walk and run
总结 :b是Animal类型,却运行了Dog类的方法,因为编译时会检查引用类型 。在运行时,JVM 会判定对象类型到底属于哪一个对象。因此,在上面的例子中,虽然 Animal有move()方法,程序会正常编译。在运行时,会运行特定对象的方法。(按Animal编译,按Dog运行)
实例2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
public class Employee { private String name; private String address; private int number; public Employee(String name, String address, int number) { System.out.println("Employee 构造函数"); this.name = name; this.address = address; this.number = number; } public void mailCheck() { System.out.println("邮寄支票给: " + this.name + " " + this.address); } public String getName() { return name; } ... } public class Salary extends Employee { private double salary; // 全年工资 public Salary(String name, String address, int number, double salary) { super(name, address, number); setSalary(salary); } public void mailCheck() { System.out.println("Salary 类的 mailCheck 方法 "); System.out.println("邮寄支票给:" + getName() + " ,工资为:" + salary); } ... } public class VirtualDemo { public static void main(String [] args) { Salary s = new Salary("员工 A", "北京", 3, 3600.00); // 子类引用指向子类对象 Employee e = new Salary("员工 B", "上海", 2, 2400.00); // 父类引用指向子类对象 System.out.println("使用 Salary 的引用调用 mailCheck -- "); s.mailCheck(); System.out.println("n使用 Employee 的引用调用 mailCheck--"); e.mailCheck(); } } //结果 Employee 构造函数 Employee 构造函数 使用 Salary 的引用调用 mailCheck -- Salary 类的 mailCheck 方法 邮寄支票给:员工 A ,工资为:3600.0 使用 Employee 的引用调用 mailCheck-- Salary 类的 mailCheck 方法 邮寄支票给:员工 B ,工资为:2400.0
实例3
News的子类UrlNews代码
为简洁,Displayable接口及实现接口的News类代码已在上面提及,不再重复展示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
public class UrlNews extends News{ private String url; // 缺省构造函数,默认调用super() public UrlNews() {} // 含参构造函数 public UrlNews(String title, String content, String url) { super(title,content); this.url = url; } public String getUrl() { return url; } // 重写 @Override public void display() { System.out.println("News from Url:" + url); super.display(); } }
测试代码
NewsReader抽象类及其UrlNewsReader子类代码已在上面提及,不再重复展示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
public class Main { public static void main(String[] args) throws IOException { // 父类引用指向父类对象 News news = new News("abc","父类"); // 父类引用指向子类对象 News urlnews = new UrlNews("ABC","子类", "class.com"); news.display(); urlnews.display(); // 实现接口的子类可以放入接口参数 viewNews(news); viewNews(urlnews); // 接口引用指向子类对象 Displayable displayable = new UrlNews("abc","def","ghi"); displayable.display(); // 抽象类引用指向子类对象 NewsReader newsReader = new UrlNewsReader(); newsReader.readNews(); } private static void viewNews(Displayable item) { item.display(); System.out.println("播放完毕"); } }
测试结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
abc // news.display(); 父类 News from Url:class.com // urlnews.display(); ABC 子类 abc // viewNews(news); 父类 播放完毕 News from Url:class.com // viewNews(urlnews); ABC 子类 播放完毕 News from Url:ghi // displayable.display(); abc def NewsReader // NewsReader newsReader = new UrlNewsReader(); UrlNewsReader Url reading...... // newsReader.readNews();
多态的好处 :利用了动态传参 ,可以避免很多重复代码例如 :在Main中定义read()方法,根据多态的原则,可以按如下方法定义:
1 2 3
private static void read(News news) { news.display(); }
如果没有多态,则需要将News类的子类的read()方法全部定义一遍:
1 2 3 4 5 6 7 8
private static void read(UrlNews urlNews) { urlNews.display(); } // FileNews是News的子类时且display()方法有重写时 private static void read(FileNews fileNews) { fileNews.display(); }
当News的子类很多时,在Main中就要写很多重复代码,浪费资源,且不方便维护。
原文:大专栏 Java面向对象编程——抽象类、接口、多态