Java面向对象编程及其三大特征

x33g5p2x  于2021-11-21 转载在 Java  
字(20.5k)|赞(0)|评价(0)|浏览(387)

一、包

包 (package) 是组织类的一种方式,使用包的主要目的是保证类的唯一性。也就是说,在同一个包底下,不能存在同名的类。相反,在不同的包底下,能够存在相同名字的类。那么与包有关的关键字有import与package 。

1.import关键字

我们都知道在Java中有许多现成的类放在不同的包底下提供给程序员使用。而类中有能够满足我们需求的实现方法。那么如何才能使用我们所需要的类呢?要使用import关键字来导入该类所在的包。例如:可以使用import java.util.Date 这种方式引入 java.util 这个包中的 Date 类。如果还需要使用java.util包下的其它类时,例如Scanner类,则还需要import java.util.Scanner

如果我们没有导入该包中的类,则还可以写成这种形式:java.util.Date date = new java.util.Date();但是这种写法比较麻烦一些, 仍然建议使用 import 语句导入包。如果我们不单单只想导入java.util当中的Date类,我们也可以写成import java.util.*,* 号代表的是通配符,顾名思义就是能够将该包下的“所有的类”都导入,为什么“所有的类”加上引号呢?这里的“所有的类”指的是我们在程序中调用了java.util包下的类时无需再导java.util.类名,而import java.util.*这种写法能够在我们使用哪个类时就导入包中指定的哪个类,只是无需再导相同的包。但它却又不是将该包底下的所有类都导入进来。

还有特殊情况:因为在不同包底下可以有相同类名,因此如果真的遇到了想调用不同包底下的相同的类时,两个包都要进行导入才能正常使用。并且要看清楚编译器提示给我们的是哪个包底下的类。调用类的方法则是 类名.方法名 。

2.静态导入

使用 import static 可以导入包中的静态的方法和字段。这种情况用的比较少。例如System类中的方法println是静态方法,因此写成下面这种形式:

import static java.lang.System.*;
public class Test {
    public static void main(String[] args) {
        out.println("hello");
   }
}

上面的例子看的有些别扭,但是导入静态的方法有时也会非常方便,例如:

import static java.lang.Math.*;
public class Test {
    public static void main(String[] args) {
        double x = 30;
        double y = 40;
        // 静态导入的方式写起来更方便一些. 
        // double result = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
        double result = sqrt(pow(x, 2) + pow(y, 2));
        System.out.println(result);
   }
}

3.package关键字

我们既然已经知道了import关键字的作用是导入一个包底下的类,而package关键字就是显示出该类是在哪个包底下的。

基本规则:

  1. 在文件的最上方加上一个 package 语句指定该代码在哪个包中
  2. 包名需要尽量指定成唯一的名字,通常会用公司的域名的颠倒形式(例如com.bit.demo1)
  3. 包名要和代码路径相匹配,例如创建 com.baidu.www 的包, 那么会存在一个对应的路径 com.baidu.www 来存储代码
  4. 如果一个类没有 package 语句, 则该类被放到一个默认包中

当我们创建了多个包时对于某一个类在哪个包底下会显得有点混乱,我们可以在该类的标签处点击鼠标右键后有如图所下的界面:

并且当创建时,编译器自动提示将该类在哪个包底下。

4.包的访问权限控制

如果某个成员不包含 public 、 private 、protected关键字, 此时这个成员可以在包内部的其他类使用, 但是不能在包外部的类使用。

下面的代码给了一个示例. Demo1 和 Demo2 是同一个包中, Test 是其他包中。

Demo1.java

package com.baidu.www;
public class Demo1 {
    int value = 0; }

Demo2.java

package com.bit.demo; 
public class Demo2 { 
 public static void Main(String[] args) { 
 Demo1 demo = new Demo1(); 
 System.out.println(demo.value); 
 } 
} 
// 执行结果, 能够访问到 value 变量
10

Test.java

import com.baidu.www.Demo1; 
public class Test { 
 public static void main(String[] args) { 
 Demo1 demo = new Demo1(); 
 System.out.println(demo.value); 
 } 
} 
// 编译出错
Error:(6, 32) java: value在com.baidu.www.Demo1中不是公共的; 无法从外部程序包中对其进行访问

二、继承

1.extends关键字

代码中创建的类, 主要是为了抽象现实中的一些事物(包含属性和方法),有的时候客观事物之间就存在一些关联关系,那么在表示成类和对象的时候也会存在一定的关联。例如, 设计一个类表示动物:

// Animal.java 
public class Animal { 
 public String name; 
 
 public Animal(String name) { 
 this.name = name; 
 } 
 
 public void eat(String food) { 
 System.out.println(this.name + "正在吃" + food); 
 } 
} 
// Cat.java 
class Cat { 
 public String name; 
 
 public Cat(String name) { 
 this.name = name; 
 } 
 
 public void eat(String food) { 
 System.out.println(this.name + "正在吃" + food); 
 } 
} 
// Bird.java 
class Bird { 
 public String name; 
 
 public Bird(String name) { 
 this.name = name; 
 } 
 
 public void eat(String food) { 
 System.out.println(this.name + "正在吃" + food); 
 } 
 
 public void fly() { 
 System.out.println(this.name + "正在飞 ︿( ̄︶ ̄)︿"); 
 } 
}

当我们观察发现:
这三个类都具备一个相同的 eat 方法, 而且行为是完全一样的
这三个类都具备一个相同的 name 属性, 而且意义是完全一样的
从逻辑上讲, Cat 和 Bird 都是一种 Animal (is - a 语义)

此时我们就可以让 Cat 和 Bird 分别继承 Animal 类, 来达到代码重用的效果。

因此可以简化成:

// Animal.java 
public class Animal { 
 public String name; 
 
 public Animal(String name) { 
 this.name = name; 
 } 
 
 public void eat(String food) { 
 System.out.println(this.name + "正在吃" + food); 
 } 
} 
// Cat.java 
class Cat extends Animal{ 
 public Cat(String name) { 
 this.name = name; 
 } 
} 
// Bird.java 
class Bird extends Animal{ 
 
 public Bird(String name) { 
 this.name = name; 
 } 
 
 public void fly() { 
 System.out.println(this.name + "正在飞 ︿( ̄︶ ̄)︿"); 
 }
}

extends 英文原意指 “扩展”. 而我们所写的类的继承, 也可以理解成基于父类进行代码上的 “扩展”。例如我们写的 Bird 类, 就是在 Animal 的基础上扩展出了 fly 方法。

2.子类及其父类的关系

在Java中,构造子类对象的过程中,是先构造父类。那么又是如何构造父类的呢?调用父类的构造方法。因此,在一个子类的构造方法中,隐藏了一个调用父类构造方法的语句,而父类的构造方法用super关键字来调用(后面会讲到)。子类是继承了父类出除了构造方法外的所有成员变量与方法。

3.protected关键字

我们发现, 如果把字段设为 private是只能在当前类使用, 因此子类不能访问. 但是设成 public, 又违背了我们 “封装” 的初衷。因此Java衍生出了protected关键字来弥补这种缺陷。
protected关键字的作用:

  1. 对于类的调用者来说, protected 修饰的字段和方法是不能访问的(在包外并且不是子类关系)
  2. 对于类的子类和同一个包的其他类 来说, protected 修饰的字段和方法是可以访问的
// Animal.java 
public class Animal { 
 protected String name; 
 public Animal(String name) {
  this.name = name; 
 } 
 public void eat(String food) { 
 System.out.println(this.name + "正在吃" + food); 
 } 
} 
// Bird.java 
public class Bird extends Animal { 
 public Bird(String name) { 
 super(name); 
 } 
 public void fly() { 
 // 对于父类的 protected 字段, 子类可以正确访问
 System.out.println(this.name + "正在飞 ︿( ̄︶ ̄)︿"); 
 } 
} 

// Test.java 和 Animal.java 不在同一个 包 之中了. 
public class Test { 
 public static void main(String[] args) { 
 Animal animal = new Animal("小动物"); 
 System.out.println(animal.name); // 此时编译出错, 无法访问 name 
 } 
}

因此对Java中四大访问修饰限定符总结:

1.private
用 private 修饰的类成员,只能被该类自身的方法访问和修改,而不能被任何其他类(包括该类的子类)访问和引用。因此,private 修饰符具有最高的保护级别。例如,设 PhoneCard 是电话卡类,电话卡都有密码,因此该类有一个密码域,可以把该类的密码域声明为私有成员。

2.friendly(默认)
如果一个类没有访问控制符,说明它具有默认的访问控制特性。这种默认的访问控制权规定,该类只能被同一个包中的类访问和引用,而不能被其他包中的类使用,即使其他包中有该类的子类。这种访问特性又称为包访问性(package private)。

同样,类内的成员如果没有访问控制符,也说明它们具有包访问性,或称为友元(friend)。定义在同一个文件夹中的所有类属于一个包,所以前面的程序要把用户自定义的类放在同一个文件夹中(Java 项目默认的包),以便不加修饰符也能运行。

3.protected
用保护访问控制符 protected 修饰的类成员可以被三种类所访问:该类自身、与它在同一个包中的其他类以及在其他包中的该类的子类。使用 protected 修饰符的主要作用,是允许其他包中它的子类来访问父类的特定属性和方法,否则可以使用默认访问控制符。

4.public
当一个类被声明为 public 时,它就具有了被其他包中的类访问的可能性,只要包中的其他类在程序中使用 import 语句引入 public 类,就可以访问和引用这个类。

类中被设定为 public 的方法是这个类对外的接口部分,避免了程序的其他部分直接去操作类内的数据,实际就是数据封装思想的体现。每个 Java 程序的主类都必须是 public 类,也是基于相同的原因。

4.final关键字

1.final关键字修饰常量:常量不可被修改,并且需要赋初值才能使用。例:

final int a = 10; 
a = 20; // 编译出错

2.final关键字修饰类:这个类不能被继承。

final public class Animal { 
 ... 
} 
public class Bird extends Animal { 
 ... 
} 
// 编译出错,Animal无法被继承

我们经常使用的String字符串类就是被final修饰的类,因此它无法被继承。

3.final关键字修饰方法:方法不能够被重写。

public class Animal { 
    final public void fun() {
    }
} 
public class Bird extends Animal { 
    public void func() {
    }
} 
// 编译出错,func方法无法被重写

三、组合

和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果。
例如:

public class Student { 
 ... 
} 
public class Teacher { 
 ... 
} 
public class School { 
 public Student[] students; 
 public Teacher[] teachers; 
}

组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段。这是我们设计类的一种常用方式之一。组合表示 has - a 语义,继承表示 is - a 语义。

四、多态

1.向上转型

我们通常实例化一个对象,都是写的这种形式:Bird bird = new Bird("圆圆");,用相同类型的引用实例对象。而此时代码写成Animal bird2 = new Bird("圆圆");,就称之为向上转型。(Animal是Bird的父类)

向上转型的形式有三种:

  1. 直接赋值
    就是上面举的例子中直接用父类引用引用子类对象
  2. 方法传参
    将子类引用传给父类引用作为参数来接收。
public class Test { 
 public static void main(String[] args) { 
 Bird bird = new Bird("圆圆"); 
 feed(bird); 
 } 
 public static void feed(Animal animal) { 
 animal.eat("谷子"); 
 } 
} 
// 执行结果
圆圆正在吃谷子
  1. 方法返回
    将子类的引用传回给类型为父类的引用。
public class Test { 
 public static void main(String[] args) { 
 Animal animal = findMyAnimal(); 
 } 
 public static Animal findMyAnimal() { 
 Bird bird = new Bird("圆圆"); 
 return bird; 
 } 
}

对于向上转型,我们只能够调用父类当中有的成员变量,而子类当中的成员变量无法访问。

2.方法重写(Overload)

子类实现父类的同名方法, 并且参数的类型和个数完全相同, 这种情况称为 覆写/重写/覆盖(Override)。
对于重写,总结出六点:

  1. 子类重写父类的方法返回类型、方法名、参数都必须相同。(除了协变类型特例,协变类型返回值是引用,可以不同)。
  2. 被final修饰的方法不能够被重写。
  3. 被static修饰的方法不能够被重写。
  4. 子类中重写的访问修饰限定符大小要大于等于父类同名方法的访问修饰限定符。
  5. private修饰的方法不能被重写。
  6. 另外, 针对重写的方法, 可以使用 @Override 注解来显式指定。

协变类型的例子:

class A {
     public String name ;
     public A func() {
          System.out.println();
     }
}
public class Test extends A{
     @Override
     public Test func() {
          System.out.println();
          return null;
     }
}

重写与重载的区别:
1.重写实现的是运行时的多态,而重载实现的是编译时的多态。 2.重写的方法参数列表必须相同;而重载的方法参数列表必须不同。 3.重写的方法的返回值类型只能是父类中的父类类型与子类中的子类类型或者跟父类中被重写的方法返回值相同,而重载的方法对返回值类型没有要求。

3.动态绑定与静态绑定

动态绑定也称为运行时绑定。静态绑定也有编译时绑定的说法。为什么会有这种说法呢?
我们对下面这个代码举例:

// Animal.java 
public class Animal { 
 protected String name; 
 public Animal(String name) { 
 this.name = name; 
 } 
 public void eat(String food) { 
 System.out.println("我是一只小动物"); 
 System.out.println(this.name + "正在吃" + food); 
 }
 } 
// Bird.java 
public class Bird extends Animal { 
 public Bird(String name) { 
 super(name); 
 } 
 public void eat(String food) { 
 System.out.println("我是一只小鸟"); 
 System.out.println(this.name + "正在吃" + food); 
 } 
} 
// Test.java 
public class Test { 
 public static void main(String[] args) { 
 Animal animal1 = new Animal("圆圆"); 
 animal1.eat("谷子"); 
 Animal animal2 = new Bird("扁扁"); 
 animal2.eat("谷子"); 
 } 
} 
// 执行结果
我是一只小动物
圆圆正在吃谷子
我是一只小鸟
扁扁正在吃谷子

当我们按住windows键+R时输入cmd,进入命令行窗口。输入命令:javap -c 类名 (反汇编命令)则有如图所示的界面:

此时我们发现编译时准备调用的是Animal中的eat方法,但是运行时最后却调用了Bird的eat方法,这种情况就称为运行时多态。因此,运行时多态是发生在重写之上的,它们的代码都相同,但是表达的侧重点不同。相反,编译时绑定就是跟重载相对应的。满足重载时调用其中的某个方法是根据传入的参数判断调用哪个方法,而我们再用相同的命令去查看反编译时而最后调用的也是该方法。因此称为编译时绑定。

4.向下转型

向上转型是子类对象转成父类对象, 向下转型就是父类对象转成子类对象。相比于向上转型来说, 向下转型没那么常见,但是也有一定的用途。

编译过程中, animal 的类型是 Animal, 此时编译器只知道这个类中有一个 eat 方法, 没有 fly 方法。
虽然 animal 实际引用的是一个 Bird 对象, 但是编译器是以 animal 的类型来查看有哪些方法的。

// (Bird) 表示强制类型转换
Bird bird = (Bird)animal; 
bird.fly(); 
// 执行结果
圆圆正在飞

但是这样的向下转型有时是不太可靠的,也不太安全。

Animal animal = new Cat("小猫"); 
Bird bird = (Bird)animal; 
bird.fly(); 
// 执行结果, 抛出异常
Exception in thread "main" java.lang.ClassCastException: Cat cannot be cast to Bird at Test.main(Test.java:35)

animal 本质上引用的是一个 Cat 对象, 是不能转成 Bird 对象的,运行时就会抛出异常,所以,为了让向下转型更安全, 我们可以先判定一下看看 animal 本质上是不是一个 Bird 实例, 再来转换。

Animal animal = new Cat("小猫"); 
if (animal instanceof Bird) { 
 Bird bird = (Bird)animal; 
 bird.fly(); 
}

instanceof 可以判定一个引用是否是某个类的实例,如果是, 则返回 true。这时再进行向下转型就比较安全了。

5.super关键字

super关键字有三种使用方式:

  1. super调用父类的成员变量:
    使用方法:super.成员
  2. super调用父类的方法:
    使用方法:super.方法
  3. super调用父类的构造方法:
    super(父类构造方法参数)

需要注意的是:用super关键字调用父类的构造方法时必须放在子类的构造方法中,有并且只能有一个super来调用父类构造方法,并且只能放在子类构造方法的第一行。

6.在构造方法中调用重写的方法

一段有坑的代码. 我们创建两个类, B 是父类, D 是子类. D 中重写 func 方法. 并且在 B 的构造方法中调用 func 。
代码示例:

class B { 
 public B() { 
 // do nothing 
 func(); 
 } 
 public void func() { 
 System.out.println("B.func()"); 
 } 
} 
class D extends B { 
 private int num = 1; 
 @Override 
 public void func() { 
 System.out.println("D.func() " + num); 
 } 
} 
public class Test { 
 public static void main(String[] args) { 
 D d = new D();
  } 
} 
// 执行结果
D.func() 0
  1. 构造 D 对象的同时, 会调用 B 的构造方法
  2. B 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 D 中的 func
  3. 此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0

7.理解多态

有了面的向上转型, 动态绑定, 方法重写之后, 我们就可以使用 多态(polypeptide) 的形式来设计程序了,我们可以写一些只关注父类的代码, 就能够同时兼容各种子类的情况。
代码示例: 打印多种形状

class Shape { 
 public void draw() { 
 // 啥都不用干
 } 
} 
class Cycle extends Shape { 
 @Override 
 public void draw() {
  System.out.println("○"); 
 } 
} 
class Rect extends Shape { 
 @Override 
 public void draw() { 
 System.out.println("□"); 
 } 
} 
class Flower extends Shape { 
 @Override 
 public void draw() { 
 System.out.println("♣"); 
 } 
} 

// Test.java 
public class Test { 
 public static void main(String[] args) { 
 Shape shape1 = new Flower(); 
 Shape shape2 = new Cycle(); 
 Shape shape3 = new Rect(); 
 drawMap(shape1); 
 drawMap(shape2); 
 drawMap(shape3); 
 } 
 // 打印单个图形
 public static void drawShape(Shape shape) { 
 shape.draw(); 
 } 
}

当类的调用者在编写 drawMap 这个方法的时候, 参数类型为 Shape (父类), 此时在该方法内部并不知道, 也不关注当前的 shape 引用指向的是哪个类型(哪个子类)的实例,此时 shape 这个引用调用 draw 方法可能会有多种不同的表现(和 shape 对应的实例相关), 这种行为就称为多态。

多态的好处有:
1.类调用者对类的使用成本进一步降低。
封装是让类的调用者不需要知道类的实现细节。
多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可。
因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低。

2.能够降低代码的 “圈复杂度”, 避免使用大量的 if - else。
例如:

public static void drawShapes() { 
 Rect rect = new Rect(); 
 Cycle cycle = new Cycle(); 
 Flower flower = new Flower(); 
 String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"}; 
 
 for (String shape : shapes) { 
 if (shape.equals("cycle")) { 
 cycle.draw(); 
 } else if (shape.equals("rect")) { 
 rect.draw(); 
 } else if (shape.equals("flower")) { 
 flower.draw(); 
 } 
 } 
}

如果我们使用多态,则代码更加简洁与增强其可读性。

public static void drawShapes() { 
 // 我们创建了一个 Shape 对象的数组. 
 Shape[] shapes = {new Cycle(), new Rect(), new Cycle(), 
 new Rect(), new Flower()}; 
 for (Shape shape : shapes) { 
 shape.draw(); 
 } 
}

3.可扩展能力更强
如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低。
对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低。
而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高。

满足多态的有最基本的三点:

  1. 父类引用 引用子类对象
  2. 父类和子类有同名的方法
  3. 用父类引用能够调用子类的覆盖方法

五、抽象类

在一个父类当中,因为每一个子类对应的覆盖方法有不同的表现形式,因此如果该方法在父类当中实现是没有意义的,因为最终会调用在子类中重写的方法。除非在子类中没有重写该方法才会调用父类的该方法。

为了解决这一问题,父类当中的方法没有必要进行实现,但是普通类当中有没有实现的方法编译器又会报错。因此衍生出了抽象方法与抽象类。

1.abstract关键字

abstract关键字可以修饰一个方法与一个类。在 draw 方法前加上 abstract 关键字, 表示这是一个抽象方法. 同时抽象方法没有方法体(没有 { }, 不能执行具体代码)
对于包含抽象方法的类, 必须加上 abstract 关键字表示这是一个抽象类。

abstract class Shape { 
 abstract public void draw(); 
}

2.抽象类中的规则及注意事项

  1. 抽象类不能够被实例化
    例如上面的Shape类,不能写为下面这种形式:但是可以向上转型。
Shape shape = new Shape(); 
// 编译出错
Error:(30, 23) java: Shape是抽象的; 无法实例化
  1. 抽象方法不能是 private 的
    如果一个方法被private修饰,则这个方法不能再被private修饰。
abstract class Shape { 
 abstract private void draw(); 
} 
// 编译出错
Error:(4, 27) java: 非法的修饰符组合: abstract和private
  1. 抽象类中可以包含其他的非抽象方法, 也可以包含字段。这个非抽象方法和普通方法的规则都是一样的, 可以被重写,也可以被子类直接调用。
abstract class Shape { 
 abstract public void draw(); 
 void func() { 
 System.out.println("func"); 
 } 
} 
class Rect extends Shape { 
 ... 
} 
public class Test { 
 public static void main(String[] args) { 
 Shape shape = new Rect(); 
 shape.func(); 
 } 
} 
// 执行结果
func
  1. 如果一个普通类继承了一个抽象类,则这个普通类需要重写这个抽象类所有的抽象方法。除非普通类也加上abstract才不用重写抽象父类的所有抽象方法。
abstract class Shape {
    abstract public void func();
}
class Circle extends Shape{
    public void func() {
        
    }
}
  1. 在4的基础上,如果已经有了一个抽象类(子类)继承了一个抽象类(父类),此时还有一个普通类继承抽象类(子类)时,需要重写抽象类(子类)和抽象类(父类)的所有抽象方法。
  2. 抽象方法与抽象类都不能被final修饰。

六、接口

接口是抽象类的更进一步. 抽象类中还可以包含非抽象方法, 和字段,而接口中包含的方法都是抽象方法, 字段只能包含静态常量。接口的出现主要是为了解决Java当中单继承的问题。

1.接口中的语法规则

  1. 使用 interface 定义一个接口。
  2. 接口中的方法一定是抽象方法, 因此可以省略 abstract。在接口当中所有的方法都默认为public abstract方法。
  3. 接口中的方法一定是 public, 因此可以省略 public。接口中没有变量,只有常量,并且默认为是被public static final修饰的常量。因为接口中所有的字段和方法都是被public修饰。因此在实现接口的类当中不能出现比public修饰符范围小的访问修饰限定符。因此在实现接口的类当中重写的方法只能被public修饰。
  4. Cycle 使用 implements 继承接口. 此时表达的含义不再是 “扩展”, 而是 “实现”。
    例如:
class Cycle implements IShape { 
 @Override 
 public void draw() { 
 System.out.println("○"); 
 } 
}
  1. 在调用的时候同样可以创建一个接口的引用, 对应到一个子类的实例。
  2. 接口不能单独被实例化,例如不能写为Shape shape = new Shape();
  3. 接口当中可以有被static修饰的方法,但其不能被重写。
  4. 一个类当中可以实现多个接口,接口与接口之间用,隔开。例如class Frog implements IRunning, ISwimming
  5. 在实现多个接口的同时,还能继承一个父类。例如:class Fish extends Animal implements ISwimming
  6. 其实接口中的所有方法也不是一定不可以实现的,如果非要实现,可以在方法前面加上default来修饰这个方法。

在接口中有不成文的规则,可以注意一下但没必要刻意。

  1. 我们创建接口的时候, 接口的命名一般以大写字母 I 开头
  2. 接口的命名一般使用 “形容词” 词性的单词
  3. 阿里编码规范中约定, 接口中的方法和属性不要加任何修饰符号, 保持代码的简洁性

一个比较容易犯的错误:看下面这行代码:

interface IShape { 
 abstract void draw() ; // 即便不写public,也是public 
} 
class Rect implements IShape { 
 void draw() { 
 System.out.println("□") ; //权限更加严格了,所以无法覆写。
 } 
}

在实现接口的类当中一定要把public加上,不能省略。

2.实现多个接口

有的时候我们需要让一个类同时继承自多个父类. 这件事情在有些编程语言通过 多继承 的方式来实现的,然而 Java 中只支持单继承, 一个类只能 extends 一个父类. 但是可以同时实现多个接口, 也能达到多继承类似的效果。
现在我们通过类来表示一组动物

class Animal { 
 protected String name; 
 
 public Animal(String name) { 
 this.name = name; 
 } 
}

另外我们再提供一组接口, 分别表示 “会飞的”, “会跑的”, "会游泳的”

interface IFlying { 
 void fly(); 
} 
interface IRunning { 
 void run(); 
} 
interface ISwimming { 
 void swim(); 
}

接下来我们创建几个具体的动物。

class Cat extends Animal implements IRunning { 
 public Cat(String name) { 
 super(name); 
 } 
 @Override 
 public void run() { 
 System.out.println(this.name + "正在用四条腿跑"); 
 } 
}

鱼, 是会游的

class Fish extends Animal implements ISwimming { 
 public Fish(String name) { 
 super(name); 
 } 
 @Override 
 public void swim() { 
 System.out.println(this.name + "正在用尾巴游泳"); 
 } 
}

青蛙, 既能跑, 又能游(两栖动物)

class Frog extends Animal implements IRunning, ISwimming { 
 public Frog(String name) { 
 super(name); 
 } 
 @Override 
 public void run() { 
 System.out.println(this.name + "正在往前跳"); 
 } 
 @Override 
 public void swim() { 
 System.out.println(this.name + "正在蹬腿游泳"); 
 } 
} 
提示, IDEA 中使用 ctrl + i 快速实现接口
还有一种神奇的动物, 水陆空三栖, 叫做 "鸭子"
class Duck extends Animal implements IRunning, ISwimming, IFlying { 
 public Duck(String name) { 
 super(name); 
 } 
 @Override 
 public void fly() { 
 System.out.println(this.name + "正在用翅膀飞"); 
 } 
 @Override 
 public void run() { 
 System.out.println(this.name + "正在用两条腿跑"); 
 } 
 @Override 
 public void swim() { 
 System.out.println(this.name + "正在漂在水上"); 
 } 
}

上面的代码展示了 Java 面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口。
继承表达的含义是 is - a 语义, 而接口表达的含义是 具有 xxx 特性。

有了接口之后,我们可以也可以实现多态。
例如, 现在实现一个方法, 叫 “散步”。

public static void walk(IRunning running) { 
 System.out.println("我带着伙伴去散步"); 
 running.run(); 
}

参数可以不是 “动物”, 只要会跑,就能够实现多态。

class Robot implements IRunning { 
 private String name; 
 public Robot(String name) { 
 this.name = name; 
 } 
 @Override 
 public void run() { 
 System.out.println(this.name + "正在用轮子跑"); 
 } 
} 
Robot robot = new Robot("机器人"); 
walk(robot); 
// 执行结果
我带着伙伴去散步
机器人正在用轮子跑

3.接口间的继承

接口继承的关键字是extends,如果一个类只实现了其中的一个接口,那么而那个接口又继承了其它的接口,那么那个类就需要重写实现的接口及其继承的接口的所有方法。

interface IRunning { 
 void run(); 
} 
interface ISwimming { 
 void swim(); 
} 
// 两栖的动物, 既能跑, 也能游
interface IAmphibious extends IRunning, ISwimming { 
} 
class Frog implements IAmphibious { 
 ... 
}

通过接口继承创建一个新的接口 IAmphibious 表示 “两栖的”. 此时实现接口创建的 Frog 类, 就继续要实现 run 方法,也需要实现 swim 方法。接口间的继承相当于把多个接口合并在一起,意为拓展。

七、接口使用实例

1.Comparable接口

以下用接口给对象数组进行排序。
给定一个学生类

class Student { 
 private String name; 
 private int score; 
 public Student(String name, int score) { 
 this.name = name; 
 this.score = score; 
 } 
 
 @Override 
 public String toString() { 
 return "[" + this.name + ":" + this.score + "]"; 
 } 
}

再给定一个学生对象数组, 对这个对象数组中的元素进行排序(按分数降序)

Student[] students = new Student[] { 
 new Student("张三", 95), 
 new Student("李四", 96), 
 new Student("王五", 97), 
 new Student("赵六", 92), 
};

按照我们之前的理解, 数组我们有一个现成的 sort 方法。但是sort方法无法知道此时Student类型是按照什么排序的。名字?分数还是年龄?

Arrays.sort(students); 
System.out.println(Arrays.toString(students)); 
// 运行出错, 抛出异常. 
Exception in thread "main" java.lang.ClassCastException: Student cannot be cast to 
java.lang.Comparable

此时引入Comparable 接口, 并实现其中的 compareTo 方法。

class Student implements Comparable { 
 private String name; 
 private int score; 
 public Student(String name, int score) { 
 this.name = name; 
 this.score = score; 
 } 
 @Override
  public String toString() { 
 return "[" + this.name + ":" + this.score + "]"; 
 } 
 @Override 
 public int compareTo(Object o) { 
 Student s = (Student)o; 
 if (this.score > s.score) { 
 return -1; 
 } else if (this.score < s.score) { 
 return 1; 
 } else { 
 return 0; 
 } 
 } 
}

在 sort 方法中会自动调用 compareTo 方法. compareTo 的参数是 Object , 其实传入的就是 Student 类型的对象。也可以在接口后面加表明比较的是Student类中的某个属性。按alt+insert重写接口当中的compareTo方法。
然后比较当前对象和参数对象的大小关系(按分数来算)。

  • 如果当前对象应排在参数对象之前, 返回小于 0 的数字;
  • 如果当前对象应排在参数对象之后, 返回大于 0 的数字;
  • 如果当前对象和参数对象不分先后, 返回 0;
    也可以直接return this.score-o.score ; 大于0则升序,小于0降序,等于0不排序。
    此时执行代码运行结果:
// 执行结果
[[王五:97], [李四:96], [张三:95], [赵六:92]]

注意事项: 对于 sort 方法来说, 需要传入的数组的每个对象都是 “可比较” 的, 需要具备 compareTo 这样的能力. 通过重写 compareTo 方法的方式, 就可以定义比较规则。如果比较的是name属性,则需要this.name.compareTo(o.name) 。

2.Comparator接口

但是Comparable接口有一个缺点,它的可入侵性非常强,假设我们要求用名字去排序,必须在原来的基础上去改变。当其他人要使用这个方法时却不知道方法内部已经被改了,会造成很多麻烦。

因此还有个接口也是对自定义类型作比较的。它是Comparator接口。当一个类实现了Comparator接口就需要重写compare方法。因此对于多个变量属性可以分成多个不同的类来实现compare方法。例如:在上面代码的基础上改为:

class Student {
    public String name ;
    public int age ;
    public double score ;
    Student(String name,int age,double score) {
        this.name=name;
        this.age=age;
        this.score=score;
    }
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", score=" + score +
                '}';
    }
}
class AgeComparator implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.age-o2.age;
    }
}
class ScoreComparator implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return (int)(o1.score-o2.score);
    }
}
class NameComparator implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.name.compareTo(o2.name);
    }
}
class TestDemo {
    public static void main(String[] args) {
        Student[] students = new Student[3];
        Student student1 = new Student("zjr",18,99);
        Student student2 = new Student("zjj",100,23);
        Student student3 = new Student("zjd",23,45);
        students[0]=student1;
        students[1]=student2;
        students[2]=student3;
        ScoreComparator scoreComparator = new ScoreComparator();
        AgeComparator ageComparator = new AgeComparator();
        NameComparator nameComparator = new NameComparator();
        Arrays.sort(students,nameComparator);
        System.out.println(Arrays.toString(students));
    }

3.Cloneable接口

对于一个自定义类型的拷贝,我们无法用 引用.clone() 方法直接进行拷贝,那么如何做到拷贝出一个新的引用并且改变原来的对象不改变新拷贝出来的对象。这种拷贝也称为深拷贝。运行Cloneable接口。

当我们实现Cloneable接口时按住ctrl键点入Cloneable接口时,我们发现是一个空接口,也称为标记接口,里面没有任何的字段和方法。标记接口的作用是说明一个类如果实现了Cloneable接口时就标明这个类是可以被克隆的。

下面的代码就能够做到对一个自定义类型进行拷贝。

class Person implements Cloneable{
    public int age;
    Student student = new Student();
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
public class Test {

    public static void main(String[] args) throws CloneNotSupportedException {
        Person person1 = new Person();
        Person person2 = (Person)person1.clone();
        System.out.println(person1.age);
        System.out.println(person2.age);
        System.out.println("===========");
        person2.age=99;
        System.out.println(person1.age);
        System.out.println(person2.age);
    }
 }

代码运行结果:

实现Cloneable接口进行拷贝有几个需要注意的点(此时单纯拷贝一个引用):

  1. 第一步:要实现Cloneable接口。
  2. 第二步:对Cloneable接口中的clone()方法进行重写。(按alt键+insert键会有提示)

main方法中如果是第一次.clone()后也会报错,此时按alt键+enter键有以下页面提示,选中第一个即可。

对于在main方法中将一个克隆后的引用强转为自定义类型是因为我们重写的方法当中返回值是Object类型。

如果一个类当中又实例化了另一个类的对象。那么我们又需要写为下面这种形式,代码示例:

class Student implements Cloneable{
    public double money = 10.0;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
class Person implements Cloneable{
    public int age;
    Student student = new Student();
    @Override
    protected Object clone() throws CloneNotSupportedException {
        //return super.clone();
        Person p = (Person)super.clone();
        p.student= (Student)this.student.clone();//对当前对象的student引用进行拷贝
        return p ;
    }
}
public class Test {

    public static void main(String[] args) throws CloneNotSupportedException {
        Person person1 = new Person();
        Person person2 = (Person)person1.clone();
        System.out.println(person1.student.money);
        System.out.println(person2.student.money);
        System.out.println("===================");
        person2.student.money=99.9;
        System.out.println(person1.student.money);
        System.out.println(person2.student.money);
    }
}

代码运行结果:

此时最好利用图解来帮助理解。
主要针对的是下面这行代码:

Person p = (Person)super.clone();
        p.student= (Student)this.student.clone();
        return p ;

这个方法是重写父类clone()方法的。理解它很重要。我们先用一个引用来接收父类克隆出来的引用,再用当前student引用来接收当前对象的student的引用,说明已经完全克隆完成,此时直接返回克隆出来的引用即可。此过程就是类似于main函数中的克隆过程。

此时最好用图来理解:

由此我们可以发现Cloneable接口实现的是深拷贝。

八、面试中Java三大特性封装、继承、多态与抽象类、接口问题

面试问题1:普通类和抽象类的区别:
答:抽象类是由abstract关键字修饰的。在抽象类中被abstract修饰的方法可以不用实现。抽象类主要是用来被继承的。如果普通子类继承了抽象类则需要重写抽象类中的abstract方法。被absract修饰的方法不能同时被final与private修饰。其余的字段与方法都与普通类的相同。

面试问题2:接口和抽象类的区别:
接口中的只有常量,默认被public static final修饰,而方法都默认被public abstract修饰。而抽象类中的抽象方法需要手动添加abstract。接口中所有的方法都需要被重写,而抽象类中的方法只有abstract方法需要被重写。接口中所有的方法都不能实现,除非加default修饰,而抽象类中只有abstract方法不用实现。

面试问题三:什么是多态?
多态指的是一种思想,它能够降低类的调用者使用类的成本。就算不知道一个引用实例的是什么对象,只要在这个对象下有父类方法的重写,则实现一个静态方法能够调用子类当中的重写。而该方法的调用的重写能够调用多种子类的对象,并且有多种表现形式,就称为多态。

相关文章