在上一篇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;
}
}
对以上内容进行测试:
public class Test {
public static void main(String[] args) {
Animal a = new Animal("father");
Dog dog = new Dog("dogname", "red"); //后边new出来的是实际指向的,左边是可以实际访问的。
Cat cat = new Cat("catname", "blue");
System.out.println(a instanceof Animal); //true
System.out.println(dog instanceof Animal); //true
System.out.println(cat instanceof Animal); //true
System.out.println(a instanceof Dog); //false
//一个父类的引用类型变量可以指向其子类的对象//子类的对象可以当作父类的来使用,属于向上转型
a = new Dog("a dog", "yellow");
//a dog, 但是一个父类的引用类型变量不可以访问其子类对象新增方法与属性,a.furColor会报错,
System.out.println(a.name);
Dog d=(Dog) a; //父类的对象当作子类来使用属于向下转型,强制转换。
System.out.println(d.name);
System.out.println(d.furColor);//yellow
}
}
再进行一个传入方法的测试:
public class Test2 {
public void f(Animal a) {
System.out.println(a.name);
if (a instanceof Dog) {
Dog d = (Dog) a;
System.out.println(d.furColor);
}
if (a instanceof Cat) {
Cat c = (Cat) a;
System.out.println(c.eyeColor);
}
}
}
public static void main(String[] args) {
Test2 t = new Test2();
Animal a = new Animal("father");
Dog d = new Dog("dogname", "blue");
Cat c = new Cat("Catname", "red");
t.f(d); //dogname;blue
t.f(c); //Catname;red
}
多态
多态是同一个行为具有多个不同表现形式或形态的能力。多态就是同一个接口,使用不同的实例而执行不同操作。多态实现的必要条件如下:
- 要有继承或者接口实现
- 要有重写,如果是继承就是重写父类方法,如果是接口就是重写接口实现的方法
- 父类引用指向子类对象,或者接口指向子类对象,总之就是向上转型。
引用只传递一个父类或者接口,但是实际当中调用的方法是根据实际类型来分配的,new的谁,指向谁,方法可以指向谁。 如果没有多态机制,还得进行判断,传入的是什么类型,再强制转换,然后才能调用方法。或者之间没有继承关系,每新增一个就要重新判断一次,重新执行一遍。继承的多态实现形式我们已经在上文看到过,这里再举个例子:
public class Animal {
public String name;
public Animal(String name) {
this.name = name;
}
public void enjoy(){
System.out.println("动物叫声");
}
}
class Dog extends Animal { //要有继承
public String furColor;
public Dog(String n, String f) {
super(n);
this.furColor = f;
}
public void enjoy(){ //要有重写
System.out.println("狗叫声 ");
}
}
//如果需要多拓展一个,只需要增加一个子类再重写一个方法即可
class Cat extends Animal {
public String eyeColor;
public Cat(String n, String e) {
super(n);
this.eyeColor=e;
}
public void enjoy(){
System.out.println("猫叫声");
}
}
测试方法如下:
public class Lady {
private String name;
private Animal pet;
public Lady(String name,Animal pet) {
this.name=name;
this.pet=pet;
}
public void mypetenjoy(){
pet.enjoy();
}
public static void main(String[] args) { //可扩展性好意思就是main里的内容和结构基本不改变,只是添加即可
Animal a=new Animal("动物"); //根据实际运行情况来调用
Cat c=new Cat("加菲", "blue");
Dog d=new Dog("汪先生","red");
Lady l1=new Lady("lilian",c); //父类引用指向子类对象
l1.mypetenjoy(); //猫叫声
Lady l2=new Lady("ranbo",d);
l2.mypetenjoy(); //狗叫声
}
}
其实多态还有接口和抽象类的实现方式:
抽象类
在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。
- 抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。
- 由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用。也是因为这个原因,通常在设计阶段决定要不要设计抽象类。
- 父类包含了子类集合的常见的方法,但是由于父类本身是抽象的,所以不能使用这些方法。
在Java中抽象类表示的是一种继承关系,一个类只能继承一个抽象类,而一个类却可以实现多个接口
public abstract class Employee
{
private String name;
private String address;
private int number;
public abstract double computePay(); //抽象方法
//其余代码
}
抽象类有如下的使用特征和注意事项,也有很多易犯错误需要注意:
- 抽象类中可以构造方法,可以存在普通属性,方法,静态属性和方法,且访问方式和普通类一样
- 抽象类中可以存在抽象方法,也不一定包含抽象方法,但是有抽象方法的类必定是抽象类。抽象类中的抽象方法只是声明,不包含方法体,就是不给出方法的具体实现也就是方法的具体功能 抽象方法不一定存在,但存在则类一定抽象,而且抽象方法只有声明
- 抽象类不能被实例化(初学者很容易犯的错),如果被实例化,就会报错,编译无法通过。只有抽象类的非抽象子类可以创建对象。
- 构造方法,类方法(用 static 修饰的方法)不能声明为抽象方法。 只有动态方法能被声明为抽象方法
- 抽象类的子类必须给出抽象类中的抽象方法的具体实现,除非该子类也是抽象类。
抽象类看,是一个未定义完整的类,其中抽象方法将会延迟到其子类中去实现。抽象类+子类实现的方法=完整的类
接口
接口在JAVA编程语言中是一个抽象类型,是抽象方法的集合,接口通常以interface来声明。
- 一个类通过实现接口的方式,从而来实现接口的抽象方法。
- 接口并不是类,编写接口的方式和类很相似,但是它们属于不同的概念。
- 类描述对象的属性和方法。接口则包含类要实现的方法。
- 除非实现接口的类是抽象类,否则该类要实现接口中的所有方法。
接口无法被实例化,但是可以被实现。一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类。另外,在 Java 中,接口类型可用来声明一个变量,他们可以成为一个空指针,或是被绑定在一个以此接口实现的对象,也就是实现多态。
[可见度] interface 接口名称 [extends 其他的接口名] {
// 声明变量
// 抽象方法
}
接口的特征
接口有如下的一些使用特征,在使用的时候需要注意:
- 在接口中只有方法的声明,没有方法体。
- 接口体只能用 public 和 abstract 修饰。only public & abstract are permitted 。 接口是隐式抽象的,当声明一个接口的时候,不必使用abstract关键字
- 在接口中只有常量,因为定义的变量,在编译的时候都会默认加上public static final
- 在接口中的方法,永远都被public来修饰,不加修饰符,默认也是public,接口中每一个方法也是隐式抽象的,声明时同样不需要abstract关键字
- 接口中没有构造方法,也不能实例化接口的对象。(所以接口不能继承类)
- 接口可以实现多继承,接口可以继承接口,用extends
- 接口中定义的方法都需要有实现类来实现,如果实现类不能实现接口中的所有方法则实现类定义为抽象类。
总体而言,实际上接口是一种特殊的抽象类
接口示例
以下是一个接口的例子,我们定义一个Singer 接口,然后它有不同的实现方式:
interface Singer {
public void sleep();
public void sing();
}
interface Painter {
public void paint();
public void eat();
}
实现类可以实现一个接口
class Student implements Singer {
private String name;
public Student(String name) {
this.name = name;
}
public String getname() {
return name;
}
public void study() {
System.out.println("studnet study");
}
@Override
public void sleep() {
System.out.println("student sleep");
}
@Override
public void sing() {
System.out.println("student sing");
}
}
实现类也可以实现多个接口:
class Teacher implements Painter, Singer {
private String name;
public Teacher(String name) {
this.name = name;
}
@Override
public void sleep() {
System.out.println("teacher sleep");
}
@Override
public void sing() {
System.out.println("teacher sing");
}
@Override
public void paint() {
System.out.println("teacher paint");
}
@Override
public void eat() {
System.out.println("teacher eat");
}
}
接口的多态实现
做一个接口的测试如下:
public class Testinterface {
public static void main(String[] args) {
Singer s1 = new Student("TML"); //引用是什么接口,那么只能访问该接口持有的方法
s1.sing();//student sing
s1.sleep(); //student sleep
Singer s2 = new Teacher("javateacher");
s2.sing(); //teacher sing
s2.sleep(); //teacher sleep
}
}
接口和抽象类的复合使用
一个类可以继承抽象类同时实现多个接口,来看这样一个复合使用过程:
interface Protectable{
public void beprotectable();
}
interface Valuable{
int a=10;//接口里定义的都是常量,必须赋值,前面默认有public static final
public int money();
}
interface A extends Protectable{ //接口可以继承接口
public void m();
}
abstract class Animal{
public String name;
public void info(){
System.out.println("我是一种动物");
}
public abstract void enjoy();
}
定义好如上的一些接口和抽象类后,我们再来看实现:
class GoldenMonker extends Animal implements Protectable,Valuable,A{//类可以继承类,也可以实现接口
@Override
public int money() { //Valuable接口
int money=10000;
return money;
}
@Override
public void beprotectable() { //Protectable接口
System.out.println("我是一个应该被保护的猴子");
}
@Override
public void enjoy() { //Animal 抽象类
System.out.println("猴子叫");
}
public GoldenMonker(String name) {
this.name=name;
}
@Override
public void m() { //A接口
System.out.println("我不是母鸡啊");
}
}
定义好实现类后来进行一个测试:
public class Testcomprehensive {
public static void main(String[] args) {
GoldenMonker gg=new GoldenMonker("monkey");
gg.info(); //我是一种动物,来自父类普通继承
System.out.println(gg.a); //10,来自接口的常量
gg.m(); //我不是母鸡啊
System.out.println(gg.money()); //10000
gg.beprotectable(); //我是一个应该被保护的猴子
Valuable v=new GoldenMonker("MONKEYMONY");
System.out.println(v.money());//接口引用可以指向实现它的对象,但只能实现它自身的方法,10000,这也就是多态实现
Protectable P=new GoldenMonker("monkeyprotect");
P.beprotectable(); //我是一个应该被保护的猴子
}
}
接口与抽象类
接口与抽象类在很多地方表现相同又有不同之处。
定义方式
首先来看看二者的相同点,总结而言就是都不能实例化
- 相同点是接口和抽象类都不能实例化
- 接口的实现类或抽象类的子类实现了接口或抽象类中的方法后才能实例化。
不同点就比较多了:
- 首先类可以实现多个接口,但只能继承一个抽象类从继承角度
- 接口体只能用 public 和 abstract 修饰,而抽象类的类访问修饰符除了(和abstract不搭的关键字列表系列)final,private,static,synchorized,native之外基本都可以从类的修饰符角度
- 抽象类可以有普通的成员变量,静态变量。而接口的变量默认为public static final,只能有静态的不可修改的变量,而且必须赋初值从变量角度
- 接口里的方法只能用public修饰,而抽象类的方法可以用除了(和abstract不搭的关键字列表系列)final,private,static,protected,synchorized,native的方法修饰符。并且接口只有未实现方法,但抽象类可以有普通方法从方法角度
- 接口不能有构造函数,抽象类可以有构造函数。从构造函数角度
- 接口可以多重实现,抽象类不可以实现多个父类
使用场合
如果发现某种东西会成为基础类,首先把其定义为接口,接口是极端的抽象类:
- 如果创建的组件会有多个版本,则最好创建抽象类,如果创建的功能将在大范围内相异的小而简练的功能块,则使用接口。如果要设计大的功能单元则使用抽象类。
- 抽象类主要用于关系密切的对象(共性大于个性),而接口适合为不相关的类提供通用功能(个性大于共性)。
- 接口多定义对象的行为,抽象类多定义对象的属性(抽象类里不应该有太多方法)
- 接口不变原则,尽量增加接口而不是更改原有接口。尽量将接口设计成功能单一的模块儿。一个接口最好只能做一件事情。
以下是我在网上看到的几个形象比喻: 1.飞机会飞,鸟会飞,他们都继承了同一个接口“飞”;但是F22属于飞机抽象类,鸽子属于鸟抽象类。 2. 就像铁门木门都是门(抽象类),你想要个门我给不了(不能实例化),但我可以给你个具体的铁门或木门(多态);而且只能是门,你不能说它是窗(单继承);一个门可以有锁(接口)也可以有门铃(多实现)。 门(抽象类)定义了你是什么,接口(锁)规定了你能做什么(一个接口最好只能做一件事,你不能要求锁也能发出声音吧(接口污染))。
使用背景
接口是设计的结果 ,抽象类是重构的结果 ,总而言之:优先定义接口,如果有多个接口实现有公用的部分,则使用抽象类,然后集成它
- 接口是核心,其定义了要做的事情,包含了许多的方法,但没有定义这些方法应该如何做。
- 如果许多类实现了某个接口,那么每个都要用代码实现那些方法
- 如果某一些类的实现有共通之处,则可以抽象出来一个抽象类,让抽象类实现接口的公用的代码,而那些个性化的方法则由各个子类去实现。
所以,抽象类是为了简化接口的实现,他不仅提供了公共方法的实现,让你可以快速开发,又允许你的类完全可以自己实现所有的方法,不会出现紧耦合的问题。