3、Java面向对象2


一、抽象

参考:
https://www.liaoxuefeng.com/wiki/1252599548343744/1260456371027744
https://book.apeland.cn/details/128/

抽象: 只定义, 不具体指某个东西。
比如, 我们写一个 Animal 类,然后我们知道每个动物都会吃,但是怎么吃? 没办法完美的描述出所有动物到底怎么吃的。所以动物的吃,只是一个抽象的概念,我们这里只能定义一个吃的概念,具体的实现要到猫 狗 这些类中去实现。

package com.xyq.bao;
public abstract class Animal { 		// 含有抽象方法的类必须是抽象类
     public abstract void chi(); 	// 抽象方法 用abstract修饰

     public void dong(){ 		// 抽象类也可以有正常的方法
         System.out.println("动物都会动。。。");
     }
 }

使用 abstract 修饰的方法不可以有方法体,直接”;” 就可以了。如果一个类有了抽象方法,那这个类必须是一个抽象类。

抽象类不可以创建对象,很简单,只告诉你动物你没办法找到一个具体实例与之对应。

既然抽象类不可以创建对象, 那抽象类怎么用呢? 抽象类必须配合其子类来使用。
抽象类的子类必须重写父类中的抽象方法。(当然子类也可以继续是一个抽象类,可以在子类的子类中实现抽象方法)。如果子类重写了父类的抽象方法,子类就是一个正常的类了。
猫:

package com.xyq.bao;
public class Cat extends Animal{ // 继承抽象类, 必须重写抽象方法
    @Override
    public void chi(){ // 猫是有确定的吃的方式的.
         System.out.println("猫吃鱼");
     }
 }

这样间接的. 我们使用抽象类就是定义必须有哪些字段和方法,对子类进行了约束要求子类必须有一个 xxx 方法.

抽象的作用: 对子类进行约束. 约束子类必须有 xxx 方法

小结:

  • 通过abstract定义的方法是抽象方法,它只有定义,没有实现。抽象方法定义了子类必须实现的接口规范;
  • 定义了抽象方法的 class 必须被定义为抽象类,从抽象类继承的子类必须实现抽象方法;
  • 抽象类也满足类的多态性;
  • 如果不实现抽象方法,则该子类仍是一个抽象类;
  • 面向抽象编程使得调用者只关心抽象方法的定义,不关心子类的具体实现。

面向抽象编程

当我们定义了抽象类Person,以及具体的StudentTeacher子类的时候,我们可以通过抽象类Person类型去引用具体的子类的实例:

Person s = new Student();
Person t = new Teacher();

这种引用抽象类的好处在于,我们对其进行方法调用,并不关心Person类型变量的具体子类型:

// 不关心Person变量的具体子类型:
s.run();
t.run();

同样的代码,如果引用的是一个新的子类,我们仍然不关心具体类型:

// 同样不关心新的子类是如何实现run()方法的:
Person e = new Employee();
e.run();

这种尽量引用高层类型,避免引用实际子类型的方式,称之为面向抽象编程。
面向抽象编程的本质就是:

  • 上层代码只定义规范(例如:abstract class Person);
  • 不需要子类就可以实现业务逻辑(正常编译);
  • 具体的业务逻辑由不同的子类实现,调用者并不关心。

二、接口

生活中接口其实可以理解为插板、USB 等,只要符合规范标准 插板可以接风扇、电冰箱、洗衣机、空调等等。
计算机领域中接口其实就是一种公共的规范标准,只要符合规范标准,大家都可以使用,Java 中接口更多的体现在对行为的抽象。
接口其实就是一种特殊的抽象类,接口里不能有成员变量并且所有的方法都是抽象方法

/**
  * 注意: 这里不能写class. 要写interface
  */
 public interface Valuable {
     void sell(); // 由于接口里所有的方法都是public abstract 所以不放修饰符,默认是全局抽象
 }

接口既然全都是抽象方法, 那如何使用呢? 和抽象类一样. 也要借助于子类。但是, 接口终归是接口, 这玩意不是类. 所以继承接口的只能是接口
image.png
image.png
image.png
image.png

1.接口继承接口

一个interface可以继承自另一个interfaceinterface继承自interface使用extends,它相当于扩展了接口的方法。例如:

interface Hello {
    void hello();
}
interface Person extends Hello {
    void run();
    String getName();
}

此时,Person接口继承自Hello接口,因此,Person接口现在实际上有 3 个抽象方法签名,其中一个来自继承的Hello接口。
那接口和类之间怎么产生关系呢?

类可以实现接口(implements), 实现和继承其实从语义上讲是一样的.

2.类实现接口

interface Person {
    void run();		//只定义不实现
    String getName();
}
class Student implements Person {
    private String name;
    public Student(String name) {
        this.name = name;
    }
	//实现run方法
    @Override
    public void run() {
        System.out.println(this.name + " run");
    }
	//实现getName方法
    @Override
    public String getName() {
        return this.name;
    }
}

我们知道,在 Java 中,一个类只能继承自另一个类,不能从多个类继承。但是,一个类可以实现多个interface,例如:

class Student implements Person, Hello { // 实现了两个interface
    ...
}

当然继承和实现可以混用,即子类继承父类并实现多个接口

public interface Valuable {
     void sell(); // 由于接口里所有的方法都是public abstract  所以不放修饰符. 默认是全局抽象
}
public class Panda extends Animal implements Valuable, Protectable {
     @Override
     public void sell() {
         System.out.println("熊猫可以卖个好价钱");
     }
     @Override
     public void protect() {
         System.out.println("熊猫应该被保护起来");
     }

     @Override
     public void chi() {
         System.out.println("熊猫吃竹子");
     }
 }
//新建一个入口.java文件
public class Test {
     public static void main(String[] args) {
         Animal ani = new Panda();
         ani.chi();

         Valuable val = new Panda();
         val.sell();

         Protectable pro = new Panda();
         pro.protect();


         Panda pan = new Panda();
         pan.chi();
         pan.protect();
         pan.sell();
     }
 }

从不同的角度看熊猫~~ 所以, 接口同样具有多态性

一个类可以实现多个无关的接口 -> 熊猫
一个接口可以被多个无关的类实现 -> 我心爱的打火机也是受保护的.

打火机:

package com.xyq.bao;
public class Lighter implements Protectable{
     @Override
     public void protect() {
         System.out.println("我的打火机独一无二");
     }
 }

测试:

package com.xyq.bao;
import javafx.scene.effect.Light;
public class Test {
     public static void main(String[] args) {
         Protectable panda = new Panda();
         Protectable lighter = new Lighter();
         // 两个完全没有关系的东西 都转型成了Protectable. 这就是接口最大的作用.
     }
 }

接口最重要的作用:把不想关的两个物体连接起来,通过接口连接.
接口里都是方法么? 不, 接口里也有变量, 但是接口里的变量全部都是全局静态常量.

//IDEA选择接口类型文件
public interface Valuable{
     double money = 100; // 相当于 public static final double money = 100;
}
public class Client{
     public static void main(String[] args){
         System.out.println(Valuable.money); //可以使用类名直接调用. 静态的
         Valuable.money = 200; // 报错. 不可以被修改
     }
 }

注意: 我们很少会在接口里设置变量. 更多的是使用接口来统一数据类型. 和对实现类进行约束.

注意区分术语:
Java 的接口特指interface的定义,表示一个接口类型和一组方法签名,而编程接口泛指接口规范,如方法签名,数据格式,网络协议等。
抽象类和接口的对比如下:

abstract class interface
继承 只能 extends 一个 class 可以 implements 多个 interface
字段 可以定义实例字段 不能定义实例字段
抽象方法 可以定义抽象方法 可以定义抽象方法
非抽象方法 可以定义非抽象方法 可以定义 default 方法

方法中传入接口实例:

//接口
public interface Cook {
    void makeFood();
}

//Main.java
public class Main {
    public static void main(String[] args) {
        invokeCook(new Cook() {
            @Override
            public void makeFood() {
                System.out.println("开始做饭了。。。");
            }
        });
        //lambda方法
        invokeCook(() -> {
            System.out.println("开始做饭了lambda方法。。。");
        });
    }

    static void invokeCook(Cook cook) {
        cook.makeFood();
    }
}

3.继承关系

合理设计interfaceabstract class的继承关系,可以充分复用代码。一般来说,公共逻辑适合放在abstract class中,具体逻辑放到各个子类,而接口层次代表抽象程度。可以参考 Java 的集合类定义的一组接口、抽象类以及具体子类的继承关系:

在使用的时候,实例化的对象永远只能是某个具体的子类,但总是通过接口去引用它,因为接口比抽象类更抽象:

List list = new ArrayList(); // 用List接口引用具体子类的实例
Collection coll = list; // 向上转型为Collection接口
Iterable it = coll; // 向上转型为Iterable接口

4.接口中使用 default 方法

在接口中,可以定义default方法。例如,把Person接口的run()方法改为default方法:

public class Main {
    public static void main(String[] args) {
        Person p = new Student("Xiao Ming");
        p.run();
    }
}
interface Person {
    String getName();
    default void run() {
        System.out.println(getName() + " run");
    }
}
class Student implements Person {
    private String name;
    public Student(String name) {
        this.name = name;
    }
    public String getName() {
        return this.name;
    }
}

运行结果:Xiao Ming run
类实现接口时可以不必覆写default方法,并且在接口中可以定义多个 default 方法。
default方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是default方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。
default方法和抽象类的普通方法是有所不同的。因为interface没有字段,default方法无法访问字段,而抽象类的普通方法可以访问实例字段。

5.类快速重写接口方法

IDEA 作为一个高级的编辑器当然为我们提供了快速生成接口方法重写的快捷方式啦,

小结

  • Java 的接口(interface)定义了纯抽象规范,一个类可以实现(implements)多个接口;
  • 接口也是数据类型,适用于向上转型和向下转型;
  • 接口的所有方法都是抽象方法,接口不能定义实例字段(接口中定义的都是全局静态常量,一般我们不会在接口中定义字段,所以有些文章中直接说接口不能定义字段);
  • 接口可以定义default方法(JDK>=1.8)。

练习:
参考:https://book.apeland.cn/details/130/
现在有两种数据库. 一个是 Mysql 数据库, 另一个是 Oracle 数据库. 请通过程序设计, 设计一个可以根据用户输入来自动选择数据库, 并执行增删改查操作.
需求:
在 main 里, 根据用户输入的编号, 系统自动创建一个 MysqlDao 或者 OracleDao, 然后执行增删改查操作.
类图地址: https://www.processon.com/view/link/5c92012fe4b01e76978642b1

三、成员变量和局部变量

参考:
https://blog.csdn.net/weixin_37012881/article/details/82699089
https://book.apeland.cn/details/131/
https://www.liaoxuefeng.com/wiki/1252599548343744/1260466215676512
成员变量和局部变量简述:
在 Java 语言里,根据定义变量位置的不同,可以将变量分成两大类:成员变量(存在于堆内存中,和类一起创建)和局部变量(存在于栈内存中,当方法执行完成,让出内存,让其他方法来使用内存)。二者的运行机制存在较大差异。

1.成员变量

类变量从该类的准备阶段起开始存在,直到系统完全销毁这个类,类变量的作用域与这个类的生存范围相同;
实例变量则从该类的实例被创建起开始存在,直到系统完全销毁这个实例,实例变量的作用域与对应实例的生存范围相同。
正是基于这个原因,可以把类变量和实例变量统称为成员变量。其中类变量可以理解为类成员变量,它作为类本身的一个成员,与类本身共存亡;实例变量则可以理解为实例成员变量,它作为实例的一个成员与实例共存亡。
只要类存在,类就可以访问类变量 ; 类.类变量
只要实例存在,实例就可以访问实例变量 ; 实例.实例变量
当然实例也可以访问类变量。但是需要注意的是因为实例不拥有类变量,所以通过实例对象来访问类变量进行操作,实际上是对类变量进行操作 ,当有其他实例来访问类变量时,访问的类变量是被实例对象访问操作过的类变量。
成员变量无需显示初始化,只要为一个类定义了类变量或实例变量,系统就会在这个类的准备阶段或创建该类的实例时进行默认初始化。

package com.xyq.bao;
public class Person {
     String name;
     public static void main(String[] args) {
         Person p = new Person();
         System.out.println(p.name);	//null
     }
 }

这个成员变量,就是创建了一下. 并没有赋值. 但是没有飘红嘛~. 编译是通过的. 我们换个例子, 大家再看一下

package com.xyq.bao;
public class Person {
     String name;
     public static void main(String[] args) {
         Person p = new Person();
         System.out.println(p.name);		//null
         String nn;
         System.out.println(nn); 	// 飘红报错
     }
 }

打印 nn 飘红报错. 为什么会这样,记住这样一句话,所有的变量, 必须先声明后赋值, 才能使用

package com.xyq.bao;
public class Person {
     String name;
     public static void main(String[] args) {
         Person p = new Person();
         System.out.println(p.name);		//null
         String nn = "aaa";
         System.out.println(nn); 	// aaa
     }
 }

不报错了.
为什么啊?我的类全局变量也没赋值啊. 为什么可以用呢? 原因是,所有的成员变量(类变量), 在写好之后 java 都会给他们一个初始值,根据数据类型的不同,给的值也不一样。
我们先给出一个 children 类

package com.xyq.bao;
public class Children {
 }

我们再来看看这些成员变量的初始值:

public class Person {
     byte b;
     short s;
     int i;
     long l;
     float f;
     double d;
     char c;
     boolean bool;
     String str;
     Children children;
     public static void main(String[] args) {
         Person p = new Person();
         System.out.println("byte="+p.b);	//byte=0
         System.out.println("short="+p.s);	//short=0
         System.out.println("int="+p.i);	//int=0
         System.out.println("long="+p.l);	//long=0
         System.out.println("float="+p.f);	//float=0.0
         System.out.println("double="+p.d);		//double=0.0
         System.out.println("char="+p.c);	//char=	 ,空值 ASCII中对应数字0
         System.out.println("boolean="+p.bool);		//boolean=false
         System.out.println("String="+p.str);	//String=null
         System.out.println("Children="+p.children);	//Children=null
     }
 }

分析结果:
char 是空字符,boolean 是 false, String 是 null. 其他都是零.
空字符在 ascii 里就是 0.
false 在计算机里也是 0.
其他的都很好解释. 唯独这个 null.

注意: null 表示空, 表示没有. 真空. 连空气都不如.

我们发现 Children 也是 null.

原因: String 其实也是一个类. 和我们写的 Children, Cat, Panda 没区别的. java 为了让程序能正常执行. 必须要给成员变量一个初始值. 但是 Children 类型的对象必须得通过 new 来创建. java 不能自动的去给你创建一个对象放这里, 所以, 只能告诉你这里的对象是空. 你用的时候啊. 自己去创建一个对象, 放这里.

2.局部变量

局部变量根据定义形式的不同,又可以分为如下三种:
形参:在定义方法签名时定义的变量,形参的作用域在整个方法中有效;
方法局部变量:在方法体内定义的局部变量,它的作用域是从定义该变量的地方生效,到该方法结束时失效;
代码块局部变量:这个局部变量的作用域从定义该变量的地方生效,到该代码结束时失效;
记住:一个变量只在一对{}中起作用。
java 允许局部变量和成员变量同名,如果方法中局部变量和成员变量同名,局部变量就会覆盖成员变量,如果需要在这个方法中引用被覆盖成员变量,则可使用 this(对于实例变量)或类名(对于类变量)作为调用者来限定访问成员变量。

局部变量作用域

下面来看看局部变量的作用域:

package abc;
public class Hello {
    void hi(String name) { // ①
        String s = name.toLowerCase(); // ②
        int len = s.length(); // ③
        if (len < 10) { // ④
            int p = 10 - len; // ⑤
            for (int i=0; i<10; i++) { // ⑥
                System.out.println(); // ⑦
            } // ⑧
        } // ⑨
    } // ⑩
}

我们观察上面的hi()方法代码:

  • 方法参数 name 是局部变量,它的作用域是整个方法,即 ① ~ ⑩;
  • 变量 s 的作用域是定义处到方法结束,即 ② ~ ⑩;
  • 变量 len 的作用域是定义处到方法结束,即 ③ ~ ⑩;
  • 变量 p 的作用域是定义处到 if 块结束,即 ⑤ ~ ⑨;
  • 变量 i 的作用域是 for 循环,即 ⑥ ~ ⑧。

使用局部变量时,应该尽可能把局部变量的作用域缩小,尽可能延后声明局部变量。

3.小结

  • Java 内建的访问权限包括publicprotectedprivatepackage权限;
  • Java 在方法内部定义的变量是局部变量,局部变量的作用域从变量声明开始,到一个块结束;
  • final修饰符不是访问权限,它可以修饰classfieldmethod
  • 一个.java文件只能包含一个public类,但可以包含多个非public类。

四、Object 对象

在 java 中, 万事万物皆为对象。
根据 xxx 是 xxxx 的关系, 很明显是一种继承关系,也即是说,所有东西都要继承对象(object)。

Object 类: java 所有类的根

任何类, 不管写不写继承关系. 最终都会默认继承 Object。
随便写个类. 创建一个对象

package com.xyq.bao;
public class Student {
     public static void main(String[] args) {
         Student s = new Student();
     }
 }

然后我们看这么一个事儿

很奇怪不是么? 我们在 student 里没有写任何东西. 为什么会有这么多的方法给我用,这些方法都是 Object 提供的.
来. 我们看看 Object 的源码. 怎么看啊? 很简单. 找个空白地方. 写 Object.然后 ctrl+左键就 OK 了

我们不用管那个 native 是什么鬼,你看这些方法是不是我们s.xxx出来的那些东西.
所以, 我们的类即使没写继承关系. 默认也会自动继承 Object.
如果我们继承其他的类比如 :Student -> Person ->Object 最终都要继承 Object;

五、Java 内存分析

Java 内存划分

在 java 中一共会分为 4 块内存区域,分别是: 堆, 栈, 代码区, 数据区.
: 主要存放对象;new Person(); new 的东西都在这里
: 局部变量,以及基础数据类型变量;
代码区: 类和方法;
数据区: 常量池,静态变量;
举个例子:

package com.xyq.bao;
public class Person {
     private String name;
     private int age;
     private String address;
     public static int num = 0;
     public Person(String name, int age, String address){
         this.name = name;
         this.age = age;
         this.address = address;
         num++;
     }

     public static void main(String[] args) {
         Person p1 = new Person("少林寺", 18, "河南嵩山");
         Person p2 = new Person("吐鲁番", 12, "新疆");
         Person p3 = new Person("海南岛", 16, "海南");
         Person p4 = new Person("北戴河", 4, "河北");
         System.out.println(Person.num);
     }
 }

接下来,画图分析一下:

示意图:

参数传递的问题

引用传递:直接把变量(内存地址)作为参数进行传递;
值传递:直接把值作为参数进行传递
java 中使用值传递,C++里有引用传递和值传递两种方式,Python 也有引用传递和值传递两种方式;
举个例子说明一下吧:

public class BianLiang {
    String name;
    int a = 10;
    public void test(int b){
        b = 20;
    }
    public static void main(String[] args) {
        BianLiang p = new BianLiang();
//        System.out.println(p.name);     //null
        //内存分析-参数传递
        p.test(p.a);
        System.out.println(p.a);		//10
    }
}

调用函数p.test(p.a)的时候对 a 在栈空间中进行了复制,然后交给 b,b 爱怎么折腾怎么折腾,对 a 没有影响。
再看一个例子:

public static void change(Cat c){
        c = new Cat("Tom","蓝色");
    }
//程序入口
Cat c = new Cat("淘气","白色");
change(c);
System.out.println(c.color);    //白色

这个原理和上面那个一模一样:

再改变一下 change 看看

public static void change(Cat c){
        c.color = "火红色";
    }
Cat c = new Cat("淘气","白色");
change(c);
System.out.println(c.color);    //火红色

六、内部类

image-20201103152450492.png
image-20201103153046527.png
image-20201103153005424.pngimage-20201103153203103.png
image.png

七、常用

equals 和==的区别

equals 是 object 里面提供的一个方法,该方法专门用来判断两个对象是否相等。用来判断字符串内容使用相同;
== 是基本运算符中的一个,用来判断左右两端内存地址是否相同;

package com.polymorphic.equals;
public class Cat {
    public String name;
    public String color;
    public Cat(String name, String color) {
        this.name = name;
        this.color = color;
    }
    public static void main(String[] args) {
        Cat c1 = new Cat("小花", "红色");
        Cat c2 = new Cat("小花", "红色");
        System.out.println(c1 == c2);   //false
        System.out.println(c1.equals(c2));  //false
    }
}

分析:分别 new 对象 c1 c2 存放在栈中的是引用地址,在堆中存放的是对象内容;

再来看一个例子:

String s1 = "xiaoming";
String s2 = "xiaoming";
String s3 = new String("xiaoming");
System.out.println(s1 == s2);       //true
System.out.println(s1 == s3);       //false
System.out.println(s1.equals(s2));  //true
System.out.println(s1.equals(s3));  //true


为什么会这样?因为字符串是我们使用频率最高的一种数据类型,java 会自动帮我们对字符串进行缓存,发现一样的字符串了就不再创建新的了。所以 s1 和 s2 内存地址是一样的,所以两个都是真. s3 在创建的时候 new 了一次,new 的时候是要创建对象的,对象再引入”xiaoming”,所以,两个对象的地址是不一样的. 但是内容是一样的。
综上, 我们在判断两个字符串是否一致的时候,一定要用 equals,这样就是很稳定的判断内容是否一样。

toString()

来打印一个类的实例化对象,看看是什么?

package com.xyq.bao;
public class Cat {
     private String name;
     private String color;
     public Cat(String name, String color) {
         this.name = name;
         this.color = color;
     }
     public static void main(String[] args) {
         Cat c = new Cat("韩梅梅", "绿色");
         System.out.println(c); // 直接打印c, com.xyq.bao.Cat@47d384ee
     }
 }

直接打印对象,这打印的是什么啊. 注意. 默认打印的内容是: 类的全名(包+类)@内存地址
看着就难受,那怎么办才能好看一些呢? **重写 toString()**就可以了

package com.xyq.bao;
public class Cat {
     private String name;
     private String color;
     public Cat(String name, String color) {
         this.name = name;
         this.color = color;
     }
     public String toString(){
         return "一只"+this.color+"猫, 名字是:"+this.name;
     }
     public static void main(String[] args) {
         Cat c = new Cat("韩梅梅", "绿色");
         System.out.println(c); 	// 一只绿色猫, 名字是:韩梅梅
     }
 }

当然强大的 IDEA 编辑器也为我们制造了快速生成重写 toString()的办法,推荐使用!

instanceof

instanceof 可以判断对象是否是某个类实例化出来的;

package com.xyq.bao;
 public class Test {
     public static void main(String[] args) {
         Animal a = new Cat();
         Cat b = new Cat();

         System.out.println(b instanceof Cat);  //true
         if(a instanceof Cat){
             System.out.println("这个动物是一只猫");		//这个动物是一只猫
         } else {
             System.out.println("这个动物不是一只猫");
         }
     }
 }

文章作者: CoderXiong
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 CoderXiong !
  目录