1 包
包 (package) 是组织类的一种方式。
包出现的目的:为了类的唯一性。
例如, 你在代码中写了一个 TestDemo类. 然后你的同事也可能写一个TestDemo类,如果出现两个同名的类, 就会冲突, 导致代码不能编译通过。
1.1 将类放入包中
基本规则:
- 在文件的最上方加上一个 package 语句指定该代码在哪个包中;
- 包名需要尽量指定成唯一的名字, 通常会用公司的域名的颠倒形式(例如 com.baidu.www ),且包名是小写的;
- 包名要和代码路径相匹配,例如创建com.baidu.www的包, 那么会存在一个对应的路径 com/baidu/www 来存储代码;
- 如果一个类没有 package 语句, 则该类被放到一个默认包中。
操作步骤:
- 在 IDEA 中先新建一个包: 右键 src -> 新建 -> 包。
- 在弹出的对话框中输入包名, 例如com.baidu.www。(到这一步时只是建立了一些文件夹而已)
- 在包中创建类, 右键包名 -> 新建 -> 类, 然后输入类名即可。
- 此时可以看到我们的磁盘上的目录结构已经被 IDEA 自动创建出来了。
- 同时我们也看到了, 在新创建的 TestDemo.java 文件的最上方, 就出现了一个 package 语句。
其他:
路径也叫做类的全限定名。例如下图中两个同为TestDemo的类是不一样的,一个路径是src.TestDemo.java,另一个是com.baidu.www.TestDemo.java。
如下图这两个类还有一个区别就是:src.TestDemo.java路径下代码编辑页面直接是所创建的类(图1),而com.baidu.www.TestDemo.java路径下代码编辑页面在所创建的类上面还有一句代码,用来指明这个类所属的包是哪一个(图2)。
图1:
图2:
1.2 导入包中的类
Java 中已经提供了很多现成的类供我们使用,也叫做系统包。例如:可以使用 java.util.Date 这种方式引入 java.util 这个包中的 Date 类和使用 java.util.Arrays 这种方式引入 java.util 这个包中的Arrays 类。
package com.baidu.www;//我们自定义的包
public class TestDemo {
public static void main(String[] args) {
java.util.Date date = new java.util.Date();
//使用 java.util.Date 这种方式引入 java.util 这个包中的 Date 类。
//得到一个毫秒级别的时间戳
System.out.println(date.getTime());
}
public static void main1(String[] args) {
int[] array = {1,2,3,4,5,6};
String str = java.util.Arrays.toString(array);
System.out.println(str);
//如果前面不引入java.util.Arrays这个系统包,我们也可以通过这种方式进行使用这个包
//但是这种方法太麻烦且不美观,所以我们一般采取在上面引入系统包的方式编写代码
}}
但是这种写法比较麻烦一些, 可以直接使用 import 语句导入包。
package com.baidu.www;//我们自定义的包
import java.util.Arrays;//系统包
import java.util.Date;
public class TestDemo {
public static void main(String[] args) {
Date date = new Date();
// 得到一个毫秒级别的时间戳
System.out.println(date.getTime());
}
public static void main1(String[] args) {
int[] array = {1,2,3,4,5,6};
Arrays.toString(array);
}}
如果需要使用 java.util 中的其他类或所有类, 可以使用 import java.util.*。
package com.baidu.www;//我们自定义的包
import java.util.*; //*代表所有,代表引入util下的所有东西
public class TestDemo {
public static void main(String[] args) {
Date date = new Date();
// 得到一个毫秒级别的时间戳
System.out.println(date.getTime());
}
public static void main1(String[] args) {
int[] array = {1,2,3,4,5,6};
Arrays.toString(array);
}}
但是更建议显式的指定要导入的类名, 否则还是容易出现冲突的情况。例如下面的代码中 util 和 sql 中都存在一个 Date 这样的类,机器并不知道你需要的是哪一个,此时就会出现歧义, 编译出错,在这种情况下需要使用完整的类名。
package com.baidu.www;//我们自定义的包
import java.util.*;
import java.sql.*;
public class TestDemo {
public static void main(String[] args) {
// util 和 sql 中都存在一个 Date 这样的类, 此时就会出现歧义, 编译出错
Date date = new Date();//这行会编译报错
// 得到一个毫秒级别的时间戳
System.out.println(date.getTime());//这行也会编译报错
}
public static void main1(String[] args) {
int[] array = {1,2,3,4,5,6};
Arrays.toString(array);
}}
编译出错截图如下图所示:
注意事项: import 和 C++ 的 #include 差别很大, C++ 必须 #include 来引入其他文件内容, 但是 Java 不需要。import 只是为了写代码的时候更方便. import 更类似于 C++ 的 namespace 和 using。
1.3 静态导入
!!这部分内容几乎不会被用到,仅作了解即可。
使用 import static 可以导入包中的静态的方法和字段,例如:我们平时写输出语句时用到的是 System.out.println( );但当下面代码中引入import static java.lang.System.*时,我们的输出语句就变成了 out.println( )。
package com.baidu.www;//我们自定义的包
import static java.lang.System.*;
public class TestDemo {
public static void main(String[] args) {
out.println("hello");
}
}
使用这种方式可以更方便的写一些代码,但不建议这样写且平时几乎没有人是这样写的。例如:求次方时会用到Math.sqrt或Math.pow这些方法,我们平时所写的语句会像下面代码中的double result = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));这个语句一样;但此时静态导入的方式写起来更方便一些,代码更简短一些,所写的语句为 double result = sqrt(pow(x, 2) + pow(y, 2)),这样就可以把前面的Math.给去掉了。
package com.baidu.www;//我们自定义的包
import static java.lang.Math.*;
public class TestDemo {
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);
}
}
1.4 常用的系统包
- java.lang:系统常用基础类(String、Object),此包从JDK1.1后自动导入。
- java.lang.reflect:java 反射编程包。
- java.net:进行网络编程开发包。
- java.sql:进行数据库开发的支持包。
- java.util:是java提供的工具程序包。(集合类等) 非常重要。
- java.io:I/O编程开发包。
1.5 包的访问权限控制
访问修饰限定符有四个:
- public
- private
- protected
- 什么都不写(默认权限->包访问控制权限,也就是只能在当前包内进行访问)
我们已经了解了类中的 public 和 private。private 中的成员只能被类的内部使用,
如果某个成员不包含 public 和 private 关键字, 此时这个成员可以在包内部的其他类使用, 但是不能在包外部的类使用。
下面给了一个示例,TestDemo和 TestDemo2 是在同一个包中, Test 是在其他包中,则
TestDemo:
TestDemo2:
此时不会报错,代码可以成功执行,能够访问到 val 变量。
Test:
此时val处会出现编译报错,代码不可以成功执行,不能够访问到 val 变量。报错原因为:val在com.baidu.www.TestDemo中不是公共的,无法从外部程序包中对其进行访问。报错截图如下图所示:
2 继承
继承: extends
继承的意义: 代码的重复使用。
面试问题:
1. 那么子类继承父类都继承了什么?
答:继承了所有的内容,除了构造方法。包含private修饰的,但是注意private修饰的不能在子类进行访问。
2.什么时候把关系设置为继承?
答:关系是 A is-a B的时候把关系设置为继承,例如:Dog is Animal。
2.1 背景
代码中创建的类, 主要是为了抽象现实中的一些事物(包含属性和方法)。有的时候客观事物之间就存在一些关联关系, 那么在表示成类和对象的时候也会存在一定的关联。
注意: 我们可以给每个类创建一个单独的 java 文件. 类名必须和 .java 文件名匹配(大小写敏感)
例如, 设计一个类表示动物:
//Animal.java
class Animal{
public String name;
public int age;
public void eat(){
//System.out.println("Animal::eat()");
System.out.println(this.name+"eat()");
}
}
//Dog.java
class Dog{
public String name;
public int age;
public void eat(){
System.out.println("Dog::eat()");
}
}
//Bird.java
class Bird{
public String name;
public int age;
public void eat(){
System.out.println("Bird::eat()");
}
public void fly(){
System.out.println("Bird::fly()");
}
}
这个代码我们发现其中存在了大量的冗余代码。仔细分析, 我们发现 Animal 和Dog以及 Bird 这几个类中存在一定的关联关系:
- 这三个类都具备一个相同的 eat 方法, 而且行为是完全一样的。
- 这三个类都具备一个相同的 name、age属性, 而且意义是完全一样的。
- 从逻辑上讲, Dog和 Bird 都是一种 Animal (is - a 语义)。
此时我们就可以让Dog 和 Bird 分别继承 Animal 类, 来达到代码重用的效果。
此时, Animal 这样被继承的类, 我们称为基类/父类/超类, 对于像 Dog和 Bird 这样的类, 我们称为子类/派生类。和现实中的儿子继承父亲的财产类似, 子类也会继承父类的字段和方法, 以达到代码重用的效果。
2.2 语法规则
基本语法:
class 子类 extends 父类 {
}
- 使用 extends 指定父类。
- Java 是单继承的,也就是一个子类只能继承一个父类 (而C++/Python等语言支持多继承)。为了解决单继承问题,Java引入了接口。
- 子类会继承父类的所有 public 的字段和方法。
- 对于父类的 private 的字段和方法, 子类中是无法访问的。
- 子类的实例中, 也包含着父类的实例,可以使用 super 关键字得到父类实例的引用。
对于上面的代码, 可以使用继承进行改进,此时我们让 Dog和 Bird 继承自 Animal 类, 那么 Dog在定义的时候就不必再写 name、age 字段和 eat 方法。
class Animal{
public String name;
public int age;
public void eat(){
//System.out.println("Animal::eat()");
System.out.println(this.name+"eat()");
}
}
class Dog extends Animal{
}
/*class Dog extends Animal,A{
}
//这样不行,会报错,Java 中一个子类只能继承一个父类 (而C++/Python等语言支持多继承)。
*/
class Bird extends Animal{
public void fly(){
System.out.println(this.name+"Bird::fly()");
}
}
public class Test {
public static void main(String[] args) {
Dog dog = new Dog();
dog.name="旺财";
dog.eat();
Bird bird = new Bird();
bird.name="八哥";
bird.eat();
}}
extends 英文原意指 “扩展”,而我们所写的类的继承, 也可以理解成基于父类进行代码上的 “扩展”,例如我们写的 Bird 类, 就是在 Animal 的基础上扩展出了 fly 方法。
如果我们把 age 改成 private, 那么此时子类就不能访问了,代码示例如下所示:
class Animal{
public String name;
private int age;
public void eat(){
//System.out.println("Animal::eat()");
System.out.println(this.name+"eat()");
}
}
class Dog extends Animal{
}
class Bird extends Animal{
public void fly(){
System.out.println(this.name+"Bird::fly()");
//System.out.println(this.age+"Bird::fly()");这里会报错,因为age是私有的,并不是没有被继承,而是在类外不可以进行访问。
}
}
public class Test {
public static void main(String[] args) {
Dog dog = new Dog();
dog.name="旺财";
dog.eat();
Bird bird = new Bird();
bird.name="八哥";
bird.eat();
} }
2.3 protected 关键字
刚才我们发现, 如果把字段设为 private, 子类不能访问。但是设成 public, 又违背了我们 “封装” 的初衷。两全其美的办法就是 protected 关键字。
- 对于类的调用者来说, protected 修饰的字段和方法是不能访问的。
- 对于类的子类和同一个包的其他类来说, protected 修饰的字段和方法是可以访问的。
我们下面可以借助代码来进行理解:
//TestDemo.java
public class TestDemo {
//int val;
protected int val;
public static void main(String[] args) {
}
//Test.java
// Test.java 和 TestDemo.java 不在同一个包之中
//super:父类对象的引用
public class Test extends TestDemo {
public void func(){
System.out.println(super.val);
//被关键字protected修饰的val的访问方法比较特殊,它需要用到super关键字,这样才会访问成功,不出现编译报错,且不能为static方法
}
public static void main(String[] args) {
TestDemo testDemo = new TestDemo();
System.out.println(testDemo.val);
//这样无论val是否被关键字protected修饰,都无法被访问,会出现编译报错
}
2.4 Java 中对于字段和方法的四种访问权限总结
Java 中对于字段和方法共有四种访问权限:
- private: 类内部能访问, 类外部不能访问。
- default:默认(也叫包访问权限): 类内部能访问, 同一个包中的类可以访问, 其他类不能访问。
- protected: 类内部能访问, 子类和同一个包中的类可以访问, 其他类不能访问。
- public : 类内部和类的调用者都能访问。
备注:
- default:包访问权限。
- 同一包中的同一类就是同一个类。
- 同一包中的不同类就像下图中com.baidu.www的包下的TestDemo和TestDemo2这两个类一样,在同一个包下,却是两个不同的类。
- 不同包中的子类,表示有继承关系。就像下图中的Test这个类继承了TestDemo这个类,但是Test这个类和TestDemo这个类不在同一个包下。
- 所以这四种访问权限:public > protected > default > private。
什么时候下用哪一种呢?
答:我们希望类要尽量做到 “封装”, 即隐藏内部实现细节, 只暴露出必要的信息给类的调用者。因此我们在使用的时候应该尽可能的使用比较严格的访问权限。例如如果一个方法能用 private, 就尽量不要用public。
另外, 还有一种 简单粗暴 的做法: 将所有的字段设为 private, 将所有的方法设为 public. 不过这种方式属于是对访问权限的滥用, 还是更希望大家能写代码的时候认真思考, 该类提供的字段方法到底给 “谁” 使用(是类内部自己用, 还是类的调用者使用, 还是子类使用)。
2.5 更复杂的继承关系
刚才我们的例子中, 只涉及到 Animal, Dog和 Bird 三种类,但是如果情况更复杂一些呢?假如针对 Cat 这种情况, 我们可能还需要表示更多种类的猫~
这个时候使用继承方式来表示, 就会涉及到更复杂的体系,例如下面的代码:
class A{
}
class B extends A{
}
class C extends B{
}
class D extends C{
}
这样的继承方式称为多层继承, 即子类还可以进一步的再派生出新的子类。
时刻牢记, 我们写的类是现实事物的抽象. 而我们真正在工作中所遇到的项目往往业务比较复杂, 可能会涉及到一系列复杂的概念, 都需要我们使用代码来表示, 所以我们真实项目中所写的类也会有很多,类之间的关系也会更加复杂。但是即使如此, 我们并不希望类之间的继承层次太复杂,一般我们不希望出现超过三层的继承关系。如果继承层次太多, 就需要考虑对代码进行重构了。
如果想从语法上进行限制继承, 就可以使用 final 关键字。也就是如果A这个类不想被继承,只要在它前面加一个 final 关键字即可。
final class A{
}
class B extends A{
//这里就会报错,因为使用 final 关键字了,A这个类就不能被继承了
}
class C extends B{
}
class D extends C{
}
2.6 final关键字
曾经我们了解过 final 关键字, 修饰一个变量或者字段的时候, 表示常量 (不能修改)。
final int a = 10;
a = 20; // 编译出错
final 关键字也能修饰类, 此时表示被修饰的类就不能被继承,被称为密封类。
final class A{
}
class B extends A{
//这里就会报错,因为使用 final 关键字了,A这个类就不能被继承了
}
final 关键字的功能是限制类被继承。“限制” 这件事情意味着 “不灵活”,在编程中, 灵活往往不见得是一件好事,灵活可能意味着更容易出错。用 final 修饰的类被继承的时候, 就会编译报错, 此时就可以提示我们这样的继承是有悖这个类设计的初衷的。
我们平时是用的 String 字符串类, 就是用 final 修饰的, 不能被继承。
3 组合
和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果。
例如表示一个学校有很多老师和学生,每个学生都有钱,如下面代码所示就是一个组合:
class Money{
}
class Student{
private Money money;
}
class Teacher{
}
class School{
private Student[] students;
private Teacher[] teachers;
}
组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段。这是我们设计类的一种常用方式之一。
大家要注意体会组合、继承两种语义的区别:
1. 组合表示has a或a part of语义。
在刚才的例子中, 我们可以理解成一个学校中 “包含” 若干学生和教师,老师、学生是学校的一部分。
2. 继承表示 is - a 语义。
在上面的 “动物和狗” 的例子中, 我们可以理解成一只狗也 “是” 一种动物。
4 super和this
4.1 this关键字
- this代表当前对象的引用。
- this()调用自己构造方法。
- this.func()访问方法。
- this.data访问属性。
4.2 super关键字
提问:如果需要在子类内部调用父类方法怎么办?
答:可以使用super 关键字。
super: 表示获取到父类实例的引用,涉及到两种常见用法。
- 使用了 super 来调用父类的构造器。
public Bird(String name) {
super(name);
}
- 使用 super 来调用父类的普通方法。
class Animal{
public String name;
private int age;
public Animal(){
}
public Animal(String name,int age) {
this.name = name;
this.age = age;
}
public void eat(){
//System.out.println("Animal::eat()");
System.out.println(this.name+"eat()");
}
}
class Dog extends Animal{
public Dog(){
//super("fafa",10);
/*super();
System.out.println("123456");
这样写是没有问题的*/
/*System.out.println("123456");
super();
这样写,一旦把super放在后面就会报错*/
//所以super一定是要被放在第一行
}
public Dog(String name,int age){
super(name, age);
}
}
class Bird extends Animal{
public void func(){
System.out.println(super.name);
super.eat();
}
/* public static void func(){
System.out.println(super.name);
super.eat();
}
这样写会报错,super不能被用在static方法当中。*/
public void fly(){
System.out.println(this.name+"Bird::fly()");
//System.out.println(this.age+"Bird::fly()");这里会报错,因为age是私有的,并不是没有被继承,而是在类外不可以进行访问。
}
}
我们要知道子类继承了父类,子类构造的时候,需要首先帮助父类进行构造。
1. 怎么帮助构造呢?
答:在子类的构造方法内部,用super()调用父类的构造方法。
2. super();//显示调用父类的构造方法,它一定是被放在第一行的。所以构造方法不是被继承的,而是在子类显示被调用的。
3. super到底是什么?
答:代表父类对象的引用。
4. 注意: super不能被用在static方法当中。
4.3 this与super的区别
super 和 this 功能有些相似, 但是还是要注意其中的区别。
5 静态、实例、构造方法的执行顺序
以下面这3段代码为例进行测试:
1.只有构造方法的代码:
class Animal{
public String name;
private int age;
public Animal(){
System.out.println("Animal()");
}
public Animal(String name,int age) {
this.name = name;
this.age = age;
System.out.println("Animal(String,int)");
}
public void eat(){
//System.out.println("Animal::eat()");
System.out.println(this.name+"eat()");
}
}
class Dog extends Animal{
public Dog(){
super();
System.out.println("Dog()");
}
public Dog(String name,int age){
super(name, age);
System.out.println("Dog(String,int)");
}
}
class Bird extends Animal{
public Bird() {
System.out.println("Bird()");
}
public Bird(String name, int age) {
super(name, age);
System.out.println("Bird(String,int)");
}
public void func(){
System.out.println(super.name);
super.eat();
}
public void fly(){
System.out.println(this.name+"Bird::fly()");
}
}
public class Test extends TestDemo {
public static void main(String[] args) {
Dog dog = new Dog();
System.out.println("===========");
Bird bird = new Bird();
System.out.println("====================");
}}
运行结果为:
2.加入静态代码块和实例代码块后的代码:
class Animal{
public String name;
private int age;
static {
System.out.println("Aniaml::static{}");
}
{
System.out.println("Aniaml::{}");
}
public Animal(){
System.out.println("Animal()");
}
public Animal(String name,int age) {
this.name = name;
this.age = age;
System.out.println("Animal(String,int)");
}
public void eat(){
//System.out.println("Animal::eat()");
System.out.println(this.name+"eat()");
}
}
class Dog extends Animal{
static {
System.out.println("Dog::static{}");
}
{
System.out.println("Dog::{}");
}
public Dog(){
super();
System.out.println("Dog()");
}
public Dog(String name,int age){
super(name, age);
System.out.println("Dog(String,int)");
}
}
class Bird extends Animal{
static {
System.out.println("Bird::static{}");
}
{
System.out.println("Bird::{}");
}
public Bird() {
System.out.println("Bird()");
}
public Bird(String name, int age) {
super(name, age);
System.out.println("Bird(String,int)");
}
public void func(){
System.out.println(super.name);
super.eat();
}
public void fly(){
System.out.println(this.name+"Bird::fly()");
}
}
public class Test extends TestDemo {
public static void main(String[] args) {
Dog dog = new Dog();
System.out.println("===========");
}}
运行结果为:3. 在main()方法中加入Bird bird = new Bird();后的代码:
class Animal{
public String name;
private int age;
static {
System.out.println("Aniaml::static{}");
}
{
System.out.println("Aniaml::{}");
}
public Animal(){
System.out.println("Animal()");
}
public Animal(String name,int age) {
this.name = name;
this.age = age;
System.out.println("Animal(String,int)");
}
public void eat(){
//System.out.println("Animal::eat()");
System.out.println(this.name+"eat()");
}
}
class Dog extends Animal{
static {
System.out.println("Dog::static{}");
}
{
System.out.println("Dog::{}");
}
public Dog(){
super();
System.out.println("Dog()");
}
public Dog(String name,int age){
super(name, age);
System.out.println("Dog(String,int)");
}
}
class Bird extends Animal{
static {
System.out.println("Bird::static{}");
}
{
System.out.println("Bird::{}");
}
public Bird() {
System.out.println("Bird()");
}
public Bird(String name, int age) {
super(name, age);
System.out.println("Bird(String,int)");
}
public void func(){
System.out.println(super.name);
super.eat();
}
public void fly(){
System.out.println(this.name+"Bird::fly()");
}
}
public class Test extends TestDemo {
public static void main(String[] args) {
Dog dog = new Dog();
System.out.println("===========");
Bird bird = new Bird();
System.out.println("====================");
}}
运行结果为:
所以综上所述:静态的先执行,且执行一次;实例的再执行;构造的最后执行。
6 多态
在了解多态之前,我们首先需要了解一下什么是向上转型和向下转型?
6.1 向上转型and向下转型
6.1.1 向上转型
向上转型: 以下面的这幅图为例,我们知道Dog属于Animal的一种,所以从Dog类型转向Animal类型就属于向上转型。
向上转型的本质: 父类引用引用子类对象。
向上转型发生的时机(共3种):
- 直接赋值 ,例:Animal animal = new Dog();。
class Animal{
public String name;
private int age;
public Animal(){
System.out.println("Animal()");
}
public Animal(String name,int age) {
this.name = name;
this.age = age;
System.out.println("Animal(String,int)");
}
public void eat(){
System.out.println(this.name+"eat()");
}
}
class Dog extends Animal{
public Dog(){
System.out.println("Dog()");
}
public Dog(String name,int age){
super(name, age);
System.out.println("Dog(String,int)");
}
public int leg;//狗腿的个数
public void wangwang(){
System.out.println("wangwang!!!");
}
}
public class Test extends TestDemo {
public static void main(String[] args) {
Dog dog = new Dog();
dog.wangwang();
Animal animal = new Dog();//这个语句和下面Animal animal = dog;这条语句是一样的,此时就为直接赋值形式的向上转型
//Animal animal = dog;
//animal.wangwang();这里编译报错的原因是animal只能访问自己的东西,不能访问子类特有的属性或者方法
//animal.leg = 9;这里编译报错的原因同上面的代码一样,是animal只能访问自己的东西,不能访问子类特有的属性或者方法
animal.name = "asdfg";
}}
- 方法传参,例: func2(animal);。
class Animal{
public String name;
private int age;
public Animal(){
System.out.println("Animal()");
}
public Animal(String name,int age) {
this.name = name;
this.age = age;
System.out.println("Animal(String,int)");
}
public void eat(){
System.out.println(this.name+"eat()");
}
}
class Dog extends Animal{
public Dog(){
System.out.println("Dog()");
}
public Dog(String name,int age){
super(name, age);
System.out.println("Dog(String,int)");
}
public int leg;//狗腿的个数
public void wangwang(){
System.out.println("wangwang!!!");
}
}
public class Test extends TestDemo {
public static void func2(Animal animal){
}
public static void main(String[] args) {
Dog dog = new Dog();
dog.wangwang();
Animal animal = new Dog();//这个语句和下面Animal animal = dog;这条语句是一样的
func2(animal);
//Animal animal = dog;
//animal.wangwang();这里编译报错的原因是animal只能访问自己的东西,不能访问子类特有的属性或者方法
//animal.leg = 9;这里编译报错的原因同上面的代码一样,是animal只能访问自己的东西,不能访问子类特有的属性或者方法
animal.name = "asdfg";
}}
- 方法的返回值,例:
public static Animal func3(Animal animal){
Dog dog = new Dog();
return new Dog();
//或return dog;
}
class Animal{
public String name;
private int age;
public Animal(){
System.out.println("Animal()");
}
public Animal(String name,int age) {
this.name = name;
this.age = age;
System.out.println("Animal(String,int)");
}
public void eat(){
System.out.println(this.name+"eat()");
}
}
class Dog extends Animal{
public Dog(){
System.out.println("Dog()");
}
public Dog(String name,int age){
super(name, age);
System.out.println("Dog(String,int)");
}
public int leg;//狗腿的个数
public void wangwang(){
System.out.println("wangwang!!!");
}
}
public class Test extends TestDemo {
public static Animal func3(Animal animal){
Dog dog = new Dog();
return new Dog();
//或return dog;
}
public static void main(String[] args) {
Dog dog = new Dog();
dog.wangwang();
Animal animal = new Dog();//这个语句和下面Animal animal = dog;这条语句是一样的
//Animal animal = dog;
//animal.wangwang();这里编译报错的原因是animal只能访问自己的东西,不能访问子类特有的属性或者方法
//animal.leg = 9;这里编译报错的原因同上面的代码一样,是animal只能访问自己的东西,不能访问子类特有的属性或者方法
animal.name = "asdfg";
}}
但是需要注意:animal只能访问自己的东西,不能访问子类特有的属性或者方法,否则会出现编译报错。
6.1.2 向下转型
向下转型: 以上面的那幅图为例,我们知道Dog属于Animal的一种,所以从Animal类型转向Dog类型就属于向下转型。一般不会用到向下转型,因为向下转型非常的不安全。
以下面代码为例就是一个向下转型的代码:
class Animal{
public String name;
private int age;
public Animal(){
System.out.println("Animal()");
}
public Animal(String name,int age) {
this.name = name;
this.age = age;
System.out.println("Animal(String,int)");
}
public void eat(){
System.out.println(this.name+"eat()");
}
}
class Dog extends Animal{
public Dog(){
System.out.println("Dog()");
}
public Dog(String name,int age){
super(name, age);
System.out.println("Dog(String,int)");
}
public int leg;//狗腿的个数
public void wangwang(){
System.out.println("wangwang!!!");
}
}
class Bird extends Animal{
public Bird() {
System.out.println("Bird()");
}
public Bird(String name, int age) {
super(name, age);
System.out.println("Bird(String,int)");
}
public void func(){
System.out.println(super.name);
super.eat();
}
public void fly(){
System.out.println(this.name+"Bird::fly()");
}
}
public class Test extends TestDemo {
public static void main(String[] args) {
Animal animal = new Dog();
Dog dog = (Dog)animal;//强制类型转换,将Animal类型转向Dog类型的向下转型
dog.wangwang();//通过强制类型转换以后这个wangwang()方法就能被访问到
}}
但是为什么说向下转型不安全呢?以下面代码为例,看似代码没有编译报错,但我们运行此代码会报错,鸟是飞不起来的。
class Animal{
public String name;
private int age;
public Animal(){
System.out.println("Animal()");
}
public Animal(String name,int age) {
this.name = name;
this.age = age;
System.out.println("Animal(String,int)");
}
public void eat(){
System.out.println(this.name+"eat()");
}
}
class Dog extends Animal{
public Dog(){
System.out.println("Dog()");
}
public Dog(String name,int age){
super(name, age);
System.out.println("Dog(String,int)");
}
public int leg;//狗腿的个数
public void wangwang(){
System.out.println("wangwang!!!");
}
}
class Bird extends Animal{
public Bird() {
System.out.println("Bird()");
}
public Bird(String name, int age) {
super(name, age);
System.out.println("Bird(String,int)");
}
public void func(){
System.out.println(super.name);
super.eat();
}
public void fly(){
System.out.println(this.name+"Bird::fly()");
}
}
public class Test extends TestDemo {
public static void main(String[] args) {
Animal animal2 = new Dog();
Bird bird = (Bird) animal2;
bird.fly();
}
}
如下图所示,此时的错误为ClassCastException,即类型转换异常。此时的异常原因为:Dog不能被转换为Bird类型。
如果此时我们需要让鸟成功飞起来,即代码可以被成功运行,需要将代码修改为下面所示的代码。
class Animal{
public String name;
private int age;
public Animal(){
System.out.println("Animal()");
}
public Animal(String name,int age) {
this.name = name;
this.age = age;
System.out.println("Animal(String,int)");
}
public void eat(){
System.out.println(this.name+"eat()");
}
}
class Dog extends Animal{
public Dog(){
System.out.println("Dog()");
}
public Dog(String name,int age){
super(name, age);
System.out.println("Dog(String,int)");
}
public int leg;//狗腿的个数
public void wangwang(){
System.out.println("wangwang!!!");
}
}
class Bird extends Animal{
public Bird() {
System.out.println("Bird()");
}
public Bird(String name, int age) {
super(name, age);
System.out.println("Bird(String,int)");
}
public void func(){
System.out.println(super.name);
super.eat();
}
public void fly(){
System.out.println(this.name+"Bird::fly()");
}
}
public class Test extends TestDemo {
public static void main(String[] args) {
Animal animal2 = new Dog();
if(animal2 instanceof Bird){
Bird bird = (Bird) animal2;
bird.fly();
}
}
}
这里我们会涉及到instanceof这个关键字,A instanceof B代表的意思是A是否是B的实例,也就是说A之前是否引用了B这个对象。
6.2 运行时绑定(动态绑定)
6.2.1 运行时绑定
运行时绑定发生的两个前提是:
- 父类引用引用子类对象。
- 通过父类引用调用父类和子类的同名覆盖方法。
此时就会发生运行时绑定(又称动态绑定),这也是多态的前提。
以下面的代码为例,就是一个运行时绑定。
- 此时代码中Dog这个类还没有eat()方法,所以 animal.eat()中调用的是Animal这个类中的eat()方法。
class Animal{
public String name;
private int age;
public Animal(){
System.out.println("Animal()");
}
public Animal(String name,int age) {
this.name = name;
this.age = age;
System.out.println("Animal(String,int)");
}
public void eat(){
System.out.println(this.name+"eat()");
}
}
class Dog extends Animal{
public Dog(){
System.out.println("Dog()");
}
public Dog(String name,int age){
super(name, age);
System.out.println("Dog(String,int)");
}
public int leg;//狗腿的个数
public void wangwang(){
System.out.println("wangwang!!!");
}
public void eat(){
System.out.println("Dog::eat()");
}
}
public class Test extends TestDemo {
public static void main(String[] args) {
Animal animal = new Dog();
//animal.wangwang();
//animal.leg = 9;
animal.name = "旺财";
animal.eat();
}}
- 此时代码中Dog这个类已经有eat()方法了,我们虽然在编译这段代码的时候仍然看到 animal.eat()调用的是Animal这个类中的eat()方法,但是我们运行这段代码的时候会发现animal.eat()调用的其实是Dog这个类中的eat()方法,此时就是发生了运行时绑定。
class Animal{
public String name;
private int age;
public Animal(){
System.out.println("Animal()");
}
public Animal(String name,int age) {
this.name = name;
this.age = age;
System.out.println("Animal(String,int)");
}
public void eat(){
System.out.println(this.name+"eat()");
}
}
class Dog extends Animal{
public Dog(){
System.out.println("Dog()");
}
public Dog(String name,int age){
super(name, age);
System.out.println("Dog(String,int)");
}
public int leg;//狗腿的个数
public void wangwang(){
System.out.println("wangwang!!!");
}
}
public class Test extends TestDemo {
public static void main(String[] args) {
Animal animal = new Dog();
//animal.wangwang();
//animal.leg = 9;
animal.name = "旺财";
animal.eat();
}}
编译这段代码的截图如下图所示:
6.2.2 重载和重写的区别
重载(overload)需要满足的三个条件:
- 方法名相同。
- 参数列表不同。
- 返回值不做要求。
它的大前提是同一个类当中。
重写(override)也叫做覆盖、覆写,它也需要满足的三个条件:
- 方法名相同。
- 参数列表相同。
- 返回值相同。(ps:注意协变类型出现的情况,下面会有详细的讲解)
它的大前提是在继承关系上(也就是父子类关系上)。上面6.2.1的示例代码中Animal这个类中的eat()方法和Dog这个类中的eat()方法就是一个重写。
当我们构成重写时,IDEA编译器会有一个向上和向下的箭头来提醒我们构成了重写,具体图标如下图所示。
在构成重写时我们需要注意以下三点:
- 子类的访问权限一定要大于等于父类的。
- 要重写的方法一定不可以是static方法。
- 要重写的方法,一定不可以被final修饰。
协变类型出现的情况:
重写的第三个条件是返回值相同,这个相同会出现下面这种情况:如果两个方法的返回值之间是构成继承关系的(这种返回值之间的关系叫做协变类型),那么我们也说它是重写的。例如下面的代码,你会发现一个方法的返回值是Animal,另一个方法的返回值是Dog,它俩的返回值并不相同,但是返回值之间是构成继承关系的,那么也叫做重写。
6.2.3 在构造方法中调用重写的方法(一个坑)
下面是一段有坑的代码,我们创建两个类, Animal是父类, Dog是子类,Dog中重写eat()方法,并且在Animal的构造方法中调用eat()方法。按理来说,执行结果中调用的应该是Animal中的eat()方法,但其实我们会发现此时调用的是Dog中的eat()方法。此时也会发生运行时绑定,这也就是一个坑。
class Animal{
public String name;
private int age;
public Animal(){
eat();
System.out.println("Animal()");
}
public Animal(String name,int age) {
this.name = name;
this.age = age;
System.out.println("Animal(String,int)");
}
public void eat(){
System.out.println(this.name+"eat()");
}
}
class Dog extends Animal{
public Dog(){
System.out.println("Dog()");
}
public Dog(String name,int age){
super(name, age);
System.out.println("Dog(String,int)");
}
public int leg;//狗腿的个数
public void wangwang(){
System.out.println("wangwang!!!");
}
public void eat(){
System.out.println("Dog::eat()");
}
}
public class Test extends TestDemo {
public static void main(String[] args) {
Animal animal = new Dog();
}}
- 构造 Dog对象的同时, 会调用Animal的构造方法。
- Animal的构造方法中调用了eat()方法, 此时会触发动态绑定, 会调用到Dog中的 eat()方法。
结论: 我们要 “用尽量简单的方式使对象进入可工作状态”, 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题。
6.3 理解多态
有了上面的向上转型, 动态绑定, 方法重写之后, 我们就可以使用多态(polypeptide) 的形式来设计程序了。我们可以写一些只关注父类的代码, 就能够同时兼容各种子类,例如:打印多种形状。
package demo1;
class Shape{
public void draw(){
}
}
class Rect extends Shape{
@Override//目的是为了验证方法是否成功进行了重写,比如下面的draw方法如果改为draw2的话,则就不是重写了,这里也就会报错了
public void draw(){
System.out.println("♦");
}
}
class Circle extends Shape{
@Override
public void draw(){
System.out.println("⚪");
}
}
class Flower extends Shape{
@Override
public void draw(){
System.out.println("❀");
}
}
/*---------------------------------------------------------------*/
public class TestDemo1 {
public static void drawMap(Shape shape){
shape.draw();
}
public static void main(String[] args) {
Rect rect = new Rect();
drawMap(rect);
Circle circle = new Circle();
drawMap(circle);
Flower flower = new Flower();
drawMap(flower);
}
}
在这个代码中, 分割线上方的代码是类的实现者编写的, 分割线下方的代码是类的调用者编写的。当类的调用者在编写 drawMap 这个方法的时候, 参数类型为 Shape (父类), 此时在该方法内部并不知道, 也不关注当前的 shape 引用指向的是哪个类型(哪个子类)的实例。此时 shape 这个引用调用 draw 方法可能会有多种不同的表现
(和 shape 对应的实例相关), 这种行为就称为多态。
也就是一个引用可以引用不同的父类对象,但是通过这一个引用来调用同样的draw方法,它会表现为不同的形态,这种思想就叫做多态。
ps:如果有人问你什么是多态?你可以这样回答:多态的发生需要有两个前提:第一个是要发生向上转型,第二个是需要发生运行时绑定。那么什么时候可以发生多态呢,它其实是一种思想,也就是说一个父类引用可以引用不同的子类对象,通过这一个父类引用可以调用同样的draw方法,可以表现为不同的状态方式,其实在这个过程中就发生了我们所谓的多态。
使用多态的好处是什么?
- 类调用者对类的使用成本进一步降低。
a. 封装是让类的调用者不需要知道类的实现细节。
b. 多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可。
因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低。这也贴合了《代码大全》中关于 “管理代码复杂程度” 的初衷。 - 能够降低代码的 “圈复杂度”, 避免使用大量的 if - else。
什么叫 “圈复杂度” ?
答:圈复杂度是一种描述一段代码复杂程度的方式。 一段代码如果平铺直叙, 那么就比较简单容易理解,而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂。因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 “圈复杂度”。如果一个方法的圈复杂度太高, 就需要考虑重构。不同公司对于代码的圈复杂度的规范不一样。一般不会超过 10,也就是if-else的个数不会超过10。
例如我们现在需要打印的不是一个形状了, 而是多个形状。如果不基于多态, 实现代码如下:
package demo1;
class Shape{
public void draw(){
}
}
class Rect extends Shape{
@Override
public void draw(){
System.out.println("♦");
}
}
class Circle extends Shape{
@Override
public void draw(){
System.out.println("⚪");
}
}
class Flower extends Shape{
@Override
public void draw(){
System.out.println("❀");
}
}
/*---------------------------------------------------------------*/
public class TestDemo1 {
public static void main(String[] args) {
Rect rect = new Rect();
Circle cycle = new Circle();
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 drawMap(Shape shape) {
shape.draw();
}
}
如果使用使用多态, 则不必写这么多的 if - else 分支语句, 代码更简单,实现代码如下:
package demo1;
class Shape{
public void draw(){
}
}
class Rect extends Shape{
@Override
public void draw(){
System.out.println("♦");
}
}
class Circle extends Shape{
@Override
public void draw(){
System.out.println("⚪");
}
}
class Flower extends Shape{
@Override
public void draw(){
System.out.println("❀");
}
}
/*---------------------------------------------------------------*/
public class TestDemo1 {
public static void main(String[] args) {
// 我们创建了一个 Shape 对象的数组.
Shape[] shapes = {new Circle(), new Rect(), new Circle(),
new Rect(), new Flower()};
for (Shape shape : shapes) {
shape.draw();
}
}
public static void drawMap(Shape shape) {
shape.draw();
}
}
- 可扩展能力更强。
如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低。具体实现代码如下:
package demo1;
class Shape{
public void draw(){
}
}
class Rect extends Shape{
@Override
public void draw(){
System.out.println("♦");
}
}
class Circle extends Shape{
@Override
public void draw(){
System.out.println("⚪");
}
}
class Flower extends Shape{
@Override
public void draw(){
System.out.println("❀");
}
}
class Triangle extends Shape{
@Override
public void draw(){
System.out.println("🔺");
}
}
/*---------------------------------------------------------------*/
public class TestDemo1 {
public static void main(String[] args) {
// 我们创建了一个 Shape 对象的数组.
Shape[] shapes = {new Circle(), new Rect(), new Circle(),
new Rect(), new Flower(),new Triangle()};
for (Shape shape : shapes) {
shape.draw();
}
}
public static void drawMap(Shape shape) {
shape.draw();
}
}
对于类的调用者来说只要创建一个新类的实例就可以了, 改动成本很低。而对于不用多态的情况, 就要把main中的 if - else 进行一定的修改, 改动成本更高。
6.4 总结
多态是面向对象程序设计中比较难理解的部分,我们会在后面的抽象类和接口中进一步体会多态的使用。重点是多态带来的编码上的好处。另一方面, 如果抛开Java, 多态其实是一个更广泛的概念, 和 “继承” 这样的语法并没有必然的联系。C++ 中的 “动态多态” 和 Java 的多态类似,但是 C++ 还有一种 “静态多态”(模板), 就和继承体系没有关系了。Python 中的多态体现的是 “鸭子类型”, 也和继承体系没有关系。Go 语言中没有 “继承” 这样的概念, 同样也能表示多态。
无论是哪种编程语言, 多态的核心都是让调用者不必关注对象的具体类型,这是降低用户使用成本的一种重要方式。
7 抽象类
抽象类和接口会在一起出现。
在刚才的打印图形例子中, 我们发现, 父类 Shape 中的 draw 方法好像并没有什么实际工作, 主要的绘制图形都是由Shape 的各种子类的 draw 方法来完成的。像这种没有实际工作的方法, 我们可以把它设计成一个 抽象方法(abstract method), 包含抽象方法的类我们称为抽象类(abstract class)。
abstract class Shape {
abstract public void draw();
}
在 draw 方法前加上 abstract 关键字, 表示这是一个抽象方法。同时抽象方法没有方法体(没有 { }, 不能执行具体代码)。
对于包含抽象方法的类, 必须加上 abstract 关键字表示这是一个抽象类。
注意事项:
- 抽象类不能进行实例化,也就是:Shape shape = new Shape();。
package demo2;
abstract class Shape{
public abstract void draw();
}
public class TestDemo {
public static void main(String[] args) {
/*Shape shape = new Shape();//这行代码会报错,因为抽象类不能进行实例化;Shape是抽象的; 无法实例化*/
}
}
- 抽象方法不能是private修饰的,因为抽象方法就是用来被重写的。
abstract class Shape {
abstract private void draw(); //编译出错,非法的修饰符组合: abstract和private
}
- 在抽象类中可以包含和普通类一样的数据成员和方法,也可以包含字段。这个非抽象方法和普通方法的规则都是一样的, 可以被重写,也可以被子类直接调用。
- 抽象类是可以被继承的,可以发生向上转型。
- 当一个普通类继承了一个抽象类,那么注意,当前这个普通类,一定要重写抽象类当中的抽象方法。
- 当普通类A继承了抽象类,且不想实现抽象类当中的抽象方法的时候,那么这个普通类可以被修改为抽象类,此时就不需要进行实现了,当然你也可以实现。如果一个普通类B,继承了这个抽象类A,此时就要实现这个抽象方法了。
package demo2;
abstract class Shape{
public int age;
public static int count;
public abstract void draw();
public void func(){
}
}
class Rect extends Shape {
@Override
public void draw(){
System.out.println("♦");
}}
class Circle extends Shape {
@Override
public void draw(){
System.out.println("⚪");
}
}
public class TestDemo {
public static void drawMap(Shape shape){
shape.draw();
}
public static void main(String[] args) {
drawMap(new Rect());
drawMap(new Circle());
}
}
/*执行结果:
♦
⚪*/
抽象类的作用:
抽象类存在的最大意义就是为了被继承。
抽象类本身不能被实例化, 要想使用, 只能创建该抽象类的子类. 然后让子类重写抽象类中的抽象方法。
有些同学可能会说了, 普通的类也可以被继承呀, 普通的方法也可以被重写呀, 为啥非得用抽象类和抽象方法呢?确实如此,但是使用抽象类相当于多了一重编译器的校验。
使用抽象类的场景就如上面的代码, 实际工作不应该由父类完成, 而应由子类完成。那么此时如果不小心误用成父类了,使用普通类编译器是不会报错的。但是父类是抽象类就会在实例化的时候提示错误,让我们尽早发现问题。很多语法存在的意义都是为了 “预防出错”, 例如我们曾经用过的 final 也是类似。创建的变量用户不去修改, 不就相当于常量嘛? 但是加上 final 能够在不小心误修改的时候, 让编译器及时提醒我们。充分利用编译器的校验, 在实际开发中是非常有意义的。
抽象类在IDEA中的图标如下图所示。
8 接口
接口是抽象类的更进一步,抽象类中还可以包含非抽象方法,和字段。而接口中包含的方法都是抽象方法, 字段只能包含静态常量。
8.1 语法规则
在刚才的打印图形的示例中, 我们的父类 Shape 并没有包含别的非抽象方法, 也可以设计成一个接口。 提示:IDEA 中使用 ctrl + i 快速实现接口。
package demo3;
interface IShape{
//简化格式
void draw();
int age = 10;
/* 完整格式:
public static final int age = 10;
public abstract void draw();*/
/* default void func(){
System.out.println("faaafa!");
}*/
}
class Rect implements IShape{
@Override
public void draw(){
System.out.println("♦");
}
}
class Circle implements IShape{
@Override
public void draw(){
System.out.println("⚪");
}
}
interface A{
public void funcA();
}
interface B{
public void funcB();
}
interface C{
public void funcC();
}
//extenda:拓展
interface D extends A,B,C{
void funcD();
}
class F implements D{
@Override
public void funcA() {
}
@Override
public void funcD() {
}
@Override
public void funcB() {
}
@Override
public void funcC() {
}
}
abstract class TestAbstract{
public abstract void funcAbstract();
}
class Test extends TestAbstract implements A,B,C{
@Override
public void funcA() {
}
@Override
public void funcB() {
}
@Override
public void funcC() {
}
@Override
public void funcAbstract() {
}
}
public class TestDemo {
public static void drawMap(IShape shape){
shape.draw();
}
public static void main(String[] args) {
//IShape iShape = new IShape();
/*IShape Shape = new Rect();//这也叫做向上转型
drawMap(Shape);*/
drawMap(new Rect());
drawMap(new Circle());
}
}
- 使用 interface 定义一个接口,即接口:使用关键字interface修饰。
- 接口当中的方法,不能有具体实现。接口当中的方法,默认是:public abstract。接口中的方法一定是抽象方法, 因此可以省略 abstract;接口中的方法一定是 public, 因此可以省略 public。
- 类和接口之间的关系是implements。Cycle 使用 implements 继承接口,此时表达的含义不再是 “扩展”, 而是 “实现”。
- 在调用的时候同样可以创建一个接口的引用, 对应到一个子类的实例。
- 接口不能单独被实例化,如:IShape iShape = new IShape()。
- 接口当中的成员变量,默认是public static final。即:接口中只能包含抽象方法,对于字段来说, 接口中只能包含静态常量(final static)。其中的 public, static, final 的关键字都可以省略,省略后的age仍然表示 public 的静态常量。
- JDK1.8引入的新特性。default修饰的方法,默认方法,可以有具体的实现。
- 一个类可以实现多个接口:class Test implements A,B,C{}。
- 一个类可以继承类,同时实现多个接口:class Test extends TestAbstract implements A,B,C{}。
- 接口可以拓展多个接口:interface D extends A,B,C{}。 接口间的继承相当于把多个接口合并在一起,所以,接口的出现就是为了解决Java多继承的问题 。
注意事项:
- 我们创建接口的时候, 接口的命名一般以大写字母 I 开头。
- 接口的命名一般使用 “形容词” 词性的单词。
- 阿里编码规范中约定, 接口中的方法和属性不要加任何修饰符号, 保持代码的简洁性。
扩展(extends) vs 实现(implements):
扩展指的是当前已经有一定的功能了, 进一步扩充功能。实现指的是当前啥都没有, 需要从头构造出来。
一个错误的代码:
interface IShape {
abstract void draw() ; // 即便不写public,也是public
}
class Rect implements IShape {
void draw() {
System.out.println("□") ; //权限更加严格了,所以无法覆写。
}
}
8.2 实现多个接口
有的时候我们需要让一个类同时继承自多个父类,这件事情在有些编程语言通过多继承的方式来实现的。然而 Java 中只支持单继承, 一个类只能 extends 一个父类。但是可以同时实现多个接口, 也能达到多继承类似的效果。
现在我们通过类来表示一组动物,另外我们再提供一组接口, 分别表示 “会飞的”, “会跑的”, “会游泳的”。接下来我们创建几个具体的动物:猫, 是会跑的;鱼, 是会游的;青蛙, 既能跑, 又能游(两栖动物);还有一种神奇的动物, 水陆空三栖, 叫做 “鸭子”,具体代码示例如下:
package 理解接口;
interface IFlying {
void fly();
}
interface IRunning {
void run();
}
interface ISwimming {
void swim();
}
class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
}
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 + "正在蹬腿游泳");
}}
/*
* 1.为什么不把IRunning, ISwimming, IFlying设置为类呢?
* 因为Java是单继承的,不能同时继承多个类。
* 所以,接口的出现就是为了解决多继承的问题。
* */
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 + "正在漂在水上");
}
}
class Robot implements IRunning {
// private String name;
/* public Robot(String name) {
this.name = name;
}*/
@Override
public void run() {
System.out.println("机器人正在用轮子跑");
}
}
public class TestDemo {
public static void goRun(IRunning iRunning) {
iRunning.run();
}
public static void goFly(IFlying iFlying) {
iFlying.fly();
}
public static void goSwim(ISwimming iSwimming) {
iSwimming.swim();
}
public static void main(String[] args) {
goRun(new Robot());
}
public static void main2(String[] args) {
goFly(new Duck("唐老鸭!"));
goRun(new Duck("唐老鸭!"));
goSwim(new Duck("唐老鸭!"));
}
public static void main1(String[] args) {
IRunning iRunning = new Duck("唐老鸭!");
iRunning.run();
IFlying iFlying = new Duck("唐老鸭!");
iFlying.fly();
ISwimming iSwimming = new Duck("唐老鸭!");
iSwimming.swim();
}
}
上面的代码展示了 Java 面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口。
继承表达的含义是 is - a 语义, 而接口表达的含义是具有 xxx 特性。即:猫是一种动物, 具有会跑的特性;青蛙也是一种动物, 既能跑, 也能游泳;鸭子也是一种动物, 既能跑, 也能游, 还能飞。
这样设计有什么好处呢? 时刻牢记多态的好处, 让程序猿忘记类型。有了接口之后, 类的使用者就不必关注具体类型, 而只关注某个类是否具备某种能力。例如, 现在实现一个方法, 叫 “goRun”,在这个goRun方法内部, 我们并不关注到底是哪种动物, 只要参数是会跑的就行。甚至参数可以不是 “动物”, 只要会跑!例如代码中的例子机器人!
8.3 常见的接口实例
给对象数组排序
给定一个学生类,再给定一个学生对象数组, 对这个对象数组中的元素进行排序(按分数排序)。按照我们之前的理解, 数组我们有一个现成的 sort 方法, 能否直接使用这个方法呢?仔细思考, 不难发现, 和普通的整数不一样, 两个整数是可以直接比较的, 大小关系明确。而两个学生对象的大小关系怎么确定? 需要我们额外指定。
方法1:Comparable接口
让我们的 Student 类实现 Comparable 接口, 并实现其中的 compareTo 方法。在 sort 方法中会自动调用 compareTo 方法. compareTo 的参数是 Object , 其实传入的就是 Student 类型的对象。然后比较当前对象和参数对象的大小关系(按分数来算):如果当前对象应排在参数对象之前, 返回大于 0 的数字; 如果当前对象应排在参数对象之后, 返回小于 0 的数字;如果当前对象和参数对象不分先后, 返回 0。再次执行程序, 结果就符合预期了。具体代码实例如下所示:
package 常见的3个接口;
import java.util.Arrays;
/*
* 对于Comparable接口来说,一般是用在类的内部的
* */
class Student implements Comparable<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 + "]";
}
@Override
public int compareTo(Student o) {
if(this.score > o.score){
return 1;
}else if(this.score == o.score){
return 0;
}else {
return -1;}
}
}
public class TestDemo {
public static void main1(String[] args) {
Student[] students = new Student[3];
students[0] = new Student("bit",89);
students[1] = new Student("abc",19);
students[2] = new Student("htf",59);
//students[0]和students[2]比较
System.out.println(students[0].compareTo(students[2]));
}
public static void main(String[] args) {
Student[] students = new Student[3];
students[0] = new Student("bit",89);
students[1] = new Student("abc",19);
students[2] = new Student("htf",59);
/* Arrays.toString(students);
System.out.println(students);
// 运行出错, 抛出异常.
*/
System.out.println( Arrays.toString(students));
System.out.println("===========排序================");
Arrays.sort(students);
System.out.println( Arrays.toString(students));
}
}
注意事项: 对于 sort 方法来说, 需要传入的数组的每个对象都是 “可比较” 的, 需要具备 compareTo 这样的能力. 通过重写 compareTo 方法的方式, 就可以定义比较规则。
方法2:比较器Comparator
通过创建比较器来实现排序,就像下面的代码我们首先创建了ScoreComparator和NameComparator两个比较器来分别根据分数进行排序和根据姓名进行排序。具体代码实例如下所示:
ScoreComparator.java
package 常见的3个接口;
import java.util.Comparator;
public class ScoreComparator implements Comparator<Student2>{
@Override
public int compare(Student2 o1, Student2 o2) {
/*if(o1.score > o2.score){
return 1;
}else if(o1.score == o2.score){
return 0;
}else{
return -1;}*/
return o1.score-o2.score;
}
}
NameComparator.java
package 常见的3个接口;
import java.util.Comparator;
public class NameComparator implements Comparator<Student2> {
@Override
public int compare(Student2 o1, Student2 o2) {
return o1.name.compareTo(o2.name);
}
}
TestDemo2.java
package 常见的3个接口;
import java.util.Arrays;
/*
* 比较器
* */
class Student2{
public int score;
public String name;
public Student2(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "[" + this.name + ":" + this.score + "]";
}
}
public class TestDemo2 {
public static void main(String[] args) {
Student2[] students = new Student2[3];
students[0] = new Student2("bit",89);
students[1] = new Student2("abc",19);
students[2] = new Student2("htf",59);
System.out.println( Arrays.toString(students));
System.out.println("===========根据分数排序================");
ScoreComparator scoreComparator= new ScoreComparator();
Arrays.sort(students,scoreComparator);
System.out.println( Arrays.toString(students));
System.out.println("===========根据姓名进行排序================");
NameComparator nameComparator= new NameComparator();
Arrays.sort(students,nameComparator);
System.out.println( Arrays.toString(students));
}
}
8.4 Clonable 接口(浅拷贝)和深拷贝
Java 中内置了一些很有用的接口, Clonable 就是其中之一。
Object 类中存在一个 clone 方法, 调用这个方法可以创建一个对象的 “拷贝”。 但是要想合法调用 clone 方法, 必须要先实现 Clonable 接口, 否则就会抛出CloneNotSupportedException 异常。具体代码实例如下所示:
package 常见的3个接口;
/*
* 1.如何拷贝对象?
* */
/*
* public interface Cloneable{}这个接口什么都没有,此时这个接口叫做空接口或者标记接口。
* 它的意义就是标记当前这个类是可以被克隆的。
* */
/*
* 克隆会做一个事情,拷贝一份副本
* */
class Person implements Cloneable{
public String name;
/*
* 生成这个方法的快捷步骤:
* Generate-->Constructor-->ok;
* */
public Person(String name) {
this.name = name;
}
/*
* 生成这个方法的快捷步骤:
* Generate-->toString-->ok;
* */
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
'}';
}
}
public class TestDemo3 {
public static void main1(String[] args) throws CloneNotSupportedException {
Person person1 = new Person("bit");
person1.clone();
Person person2 =(Person) person1.clone();
System.out.println(person1.name);//bit
System.out.println(person2.name);//bit
System.out.println("========================修改name==============================");
person2.name = "feihan";
System.out.println(person1.name);//bit
System.out.println(person2.name);//feihan
}
}
这是一个简单的浅拷贝,代码的具体理解可以通过下图进行理解。
下面这个代码实现的则是一个深拷贝:
package 常见的3个接口;
class Money implements Cloneable{
public double money = 12.8;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Person implements Cloneable{
public String name;
public Money m = new Money();
public Person(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
'}';
}
/*
* 生成这个方法的快捷步骤:
* Generate-->Override Methods-->clone()-->ok;
* */
//这里是深拷贝
@Override
protected Object clone() throws CloneNotSupportedException {
Person person2 = (Person) super.clone();
person2.m = (Money) this.m.clone();
return person2;
}
}
public class TestDemo3 {
public static void main(String[] args) throws CloneNotSupportedException {
Person person1 = new Person("bit");
person1.clone();
Person person2 =(Person) person1.clone();
System.out.println(person1.m.money);//12.8
System.out.println(person2.m.money);//12.8
System.out.println("========================修改m==============================");
person2.m.money = 99.99;
System.out.println(person1.m.money);
System.out.println(person2.m.money);
}
}
代码的具体理解可以通过下图进行理解:
浅拷贝 VS 深拷贝
Cloneable 拷贝出的对象是一份 “浅拷贝”,通过 clone 拷贝出的 Person 对象只是拷贝了 Person自身, 而没有拷贝内部包含的 Money 对象. 此时 Person1和 Person2 中包含的 Money 引用仍然是指向同一个对象,此时修改一边, 另一边也会发生改变。但是通过深拷贝实现,此时修改一边, 另一边就不会发生改变。
8.5 总结
抽象类和接口都是 Java 中多态的常见使用方式,都需要重点掌握,同时又要认清两者的区别(重要!!! 常见面试题).
核心区别:
抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写), 而接口中不能包含普通方法, 子类必须重写所有的抽象方法。如之前写的 Animal 例子,此处的 Animal 中包含一个 name 这样的属性, 这个属性在任何子类中都是存在的,因此此处的 Animal 只能作为一个抽象类, 而不应该成为一个接口。
class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
}
再次提醒:
抽象类存在的意义是为了让编译器更好的校验, 像 Animal 这样的类我们并不会直接使用, 而是使用它的子类。万一不小心创建了 Animal 的实例, 编译器会及时提醒我们。