在上一篇blog里详细介绍了面向对象的特性和原则,以及类的模型结构,本篇blog来详细介绍下Java是如何实现面向对象的几大特性:封装、继承、多态。
- 封装;隐藏实现细节,对外提供公共的访问接口,增强代码的可维护性
- 继承:最大的好处就是代码复用,同时也是多态的一个前提。
- 多态:同一个接口,使用不同的实例,父类子类,抽象类,接口。都能够实现多态(一定会有个继承关系,一定会有一个重写关系,一定会有一个子类向父类赋值或者是实现类向接口赋值),以不修改原有代码的方式增加代码的功能扩展性。
接下来就这三大特性进行详细的了解。
封装
在面向对象程式设计方法中,封装是指一种将抽象性函式接口的实现细节部分包装、隐藏起来的方法。封装可以被认为是一个保护屏障,防止该类的代码和数据被外部类定义的代码随机访问。要访问该类的代码和数据,必须通过严格的接口控制。
/* 文件名: EncapTest.java */ public class EncapTest{ private String name; private String idNum; private int age; public int getAge(){ return age; } public String getName(){ return name; } public String getIdNum(){ return idNum; } public void setAge( int newAge){ age = newAge; } public void setName(String newName){ name = newName; } public void setIdNum( String newId){ idNum = newId; } }
以上实例中public方法是外部类访问该类成员变量的入口。通常情况下,这些方法被称为getter和setter方法。因此,任何要访问类中私有成员变量的类都要通过这些getter和setter方法。
- 控制存取属性值的语句来避免对数据的不合理的操作
- 一个封装好的类,是非常容易使用的
- 代码更加模块化,增强可读性
- 隐藏类的实现细节,让使用者只能通过程序员规定的方法来访问数据
这样如果成员变量有什么修改,只需要在getter或者setter方法做修改即可,这样也可以屏蔽细节,增加可维护性。
继承
什么是继承,继承就是子类继承父类的内容并在父类之上发扬光大,子类比父类大。子类可以继承父类的一切方法,成员变量,甚至是私有的,但是却不能够访问这些私有的成员变量和方法,
Java中使用extends关键字来实现类的继承机制:
class Father { } class Child extends Father { }
需要注意的是Java只支持单继承,不支持多继承,一个子类只能有一个父类,一个父类可以有多个子类。可以看一个示例:
//父类 public class Person { private int age; private String name; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public void showinfo() { String i = "Person [age=" + age + ", name=" + name + "]"; System.out.println(i); } }
//子类,继承自Person 父类 public class Student extends Person { private String school; public String getSchool() { return school; } public void setSchool(String school) { this.school = school; } }
测试代码如下,子类可以直接调用父类的方法
public class Test extends packageA.Father{ public static void main(String[] args){ Student student=new Student(); student.setAge(22); student.setName("田茂林"); student.setSchool("CUGB"); student.showinfo(); System.out.println(student.getSchool()); } }
super关键字
super关键字表示在子类中访问父类的方法和成员,前文提到过在类的构造方法中,通过super语句调用这个类的父类的构造方法。在构造方法中,super语句必须作为构造方法的第一条语句。其实super除了构造方法还可以访问父类中的其它内容:
class Animal { void eat() { System.out.println("animal : eat"); } } class Dog extends Animal { void eat() { System.out.println("dog : eat"); } void eatTest() { this.eat(); // this 调用自己的方法 super.eat(); // super 调用父类方法 } } public class Test { public static void main(String[] args) { Dog d = new Dog(); d.eatTest(); } }
输出结果为:
dog : eat animal : eat
super关键字使用场景和注意事项如下:
- 在子类构造方法中调用父类的构造方法,在构造方法中,super语句必须作为构造方法的第一条语句。访问父类构造方法时,super必须在第一行,还有一些说明:Java会默认给类添加一个缺省构造方法,构造方法可以重载多个,同类中可以相互通过显式this调,但必须放在方法体第一行;子类在使用时默认自动super父类的缺省构造方法,如果父类没有缺省构造方法换言之就是父类显示声明了构造方法,则如果声明的构造方法无参不需要显式super,如果有参必须显式super
- 在子类中访问父类的被屏蔽的方法和属性,访问父类中同名变量或者方法,但是不能方位父类的被限制访问的私有方法和属性例如private或有些时候可能是default的,例如父类子类不在一个包。super不能访问被限制的父类方法和成员变量,例如private或default
- 只能在构造方法或实例方法内使用super关键字。也就是说当子类方法中的局部变量或者子类的成员变量与父类成员变量同名时,也就是子类变量覆盖同名父类变量时,可以使用super.成员变量名引用父类成员变量。同时,若子类的成员方法覆盖了父类的成员方法时,也可以使用super.方法名(参数列表)的方式访问父类的方法。super可以指代父类方法及变量避免混淆
- super关键字只能指代直接父类,不能指代父类的父类。super不能越级访问祖父类
以上就是super的一些使用场景。
方法的重写
重写是子类实现父类的方法,覆盖父类的原有实现,使用子类自己的实现,也是多态的一种方式,在子类中可以根据需要对从父类中继承来的方法进行重写。重写有以下原则:
- 重写的方法和被重写的方法必须具有相同方法名称、参数列表和返回类型,相同的方法签名和返回类型
- 重写方法不能使用比被重写的方法更严格的访问权限。也就是如果父类是public,子类最多就是public,访问限制修饰必须宽松于父类
- 子类方法抛出的异常必须和父类方法抛出的异常相同,或者是父类方法抛出的异常类的子类。异常必须小于父类
- 父类的静态方法是不能被子类覆盖为非静态方法,父类的非静态方法不能被子类覆盖为静态方法。重写不能修改方法类型【静态方法、成员方法】
当然还有一些注意事项,出现问题时方便排查
- 子类可以定义与父类的静态方法同名的静态方法,以便在子类中隐藏父类的静态方法。区别:运行时,JVM把静态方法和所属的类绑定,而把实例方法和所属的实例绑定。子类定义与父类同名的静态方法但不是覆盖父类的方法。运行时静态方法看引用,动态方法看实例
- 父类的私有方法不能被重写,推荐这么做,子类重写父类私有方法编译可能不会报错,但是也不算是重写,只能说是自己实现的私有方法。
- 父类的非抽象方法可以被重写为抽象方法,但是不推荐这么做
- 子类可以重写父类的同步方法。如果父类中的某个方法使用了 synchronized关键字,而子类中也覆盖了这个方法,默认情况下子类中的这个方法并不是同步的,必须显示的在子类的这个方法中加上 synchronized关键字才可。当然,也可以在子类中调用父类中相应的方法,这样虽然子类中的方法并不是同步的,但子类调用了父类中的同步方法,也就相当子类方法也同步了
重写和我们之前用到的重载有什么区别呢?
- 重载是同一个类中方法之间的关系,重写是子类和父类之间的关系
- 重写只能由一个方法或只能由一对方法产生关系,重载可以是多个方法之间的关系
- 重写要求参数列表相同,重载要求不同,重写要求返回值相同,重载不要求
其实都属于多态的实现方式,这个后续在JVM内存中再讨论。
Object类
Java Object 类是所有类的父类,也就是说 Java 的所有类都继承了 Object,子类可以使用 Object 的所有方法。
它有11个默认方法,方法列表如下:
equals和hashCode
equals和hashCode都会用于比较方法,用于比较两个对象是否相等。
- equals() 方法比较两个对象,是判断两个对象引用指向的是同一个对象,即比较 2 个对象的内存地址是否相等。也就是说即使两个对象相等,但是内存地址不同,引用指向的不是同一个也不能说两个对象相等,所以equals一般需要重写
- hashCode()方法是求出两个对象的hash值然后进行比较,equals相等hashCode一定相等,反之不成立。
注意:如果子类重写了 equals() 方法,就需要重写 hashCode() 方法,比如 String 类就重写了 equals() 方法,同时也重写了 hashCode() 方法。
clone方法
在实际编程过程中,我们常常要遇到这种情况:有一个对象A,在某一时刻A中已经包含了一些有效值,此时可能会需要一个和A完全相同新对象B,并且此后对B任何改动都不会影响到A中的值,A与B是两个独立的对象。要说明的有两点:
- 拷贝对象返回的是一个新对象,而不是一个引用。
- 拷贝对象与用 new操作符返回的新对象的区别就是这个拷贝已经包含了一些原来对象的信息,而不是对象的初始信息。
也就是实例的拷贝,而不仅仅是应用的指向,拷贝一般分为深拷贝和浅拷贝
- 浅复制:直接将源对象中的字段name的引用值拷贝给新对象的name字段
- 深复制:根据原Person对象中的name指向的字符串对象创建一个新的相同的字符串对象,将这个新字符串对象的引用赋给新拷贝的Person对象的name字段。
以下为深拷贝和浅拷贝的示意图:
总而言之,就是clone的对象的成员变量在堆内存上是否也被真实的clone了一份。
package test; import java.util.Date; /** * @author 田茂林 * @data 2017年9月6日 下午9:46:42 */ public class Person implements Cloneable { // 实现Cloneable接口,接口没有任何实现方法 private int age; private Date date = new Date(); public Date getDate() { return date; } @SuppressWarnings("deprecation") public void changeDate(){ this.date.setMonth(5); } public void setDate(Date date) { this.date = date; } @Override protected Object clone() { // 重写clone方法,注意是被保护方法 Person p = null; try { p = (Person) super.clone(); // 实现浅复制 } catch (CloneNotSupportedException e) { e.printStackTrace(); //这里要有异常捕获 } p.date = (Date) this.getDate().clone();// 实现深复制 return p; } public static void main(String[] args) { Person p = new Person(); Person p1 = (Person) p.clone(); p1.changeDate(); System.out.println("p="+p.age); System.out.println("p1="+p1.age); System.out.println("p="+p.getDate()); System.out.println("p1="+p1.getDate()); } }
运行结果
p=0 p1=0 p=Thu Sep 07 11:52:09 CST 2017 p1=Wed Jun 07 11:52:09 CST 2017
可以看到改变里边日期的值,对原来的没影响
对象转型
在【Java SE基础 二】Java基本语法这篇blog里我们聊到了基本数据类型的转型,其实引用数据类型也存在对象转型
- 一个父类的引用可以指向其子类的对象
- 一个父类的引用不可以访问其子类新增加的成员(方法和成员变量)
- 可以使用
引用变量 instanceof 类名
来判断该引用变量所指向对象是否属于该类或该类的子类,例如a instanceof Annimal a
是不是动物类中的一个实例 - 父类的引用类型变量可以指向其子类的对象叫做向上转型,子类的引用类型变量可以指向其父类的对象叫做向下转型,向下转型需要强制转换(对应于基本数据类型会导致精度丢失)
举个转型的实例如下:
//父类Animal public class Animal { public String name; public Animal(String name) { this.name = name; } } //子类Dog class Dog extends Animal { public String furColor; //子类dog的属性furColor public Dog(String n, String f) { super(n); this.furColor = f; } } //子类Cat class Cat extends Animal { public String eyeColor; //子类cat的新属性eyecolor public Cat(String n, String e) { super(n); this.eyeColor=e; } }