第五章(面向对象)

第六章 面向对象

# 一、面向对象概述

# 1、面向对象介绍

java是一门面向对象的语言,在java里一切皆对象。

# (1)面向过程和面向对象

  • 面向过程——步骤化

    面向过程就是分析出实现需求所需要的步骤,通过函数(方法)一步一步实现这些步骤,接着依次调用即可。

  • 面向对象——行为化(概念相对抽象,可结合下面的例子理解)

    面向对象是把整个需求按照特点、功能划分,将这些存在共性的部分封装成类(类实例化后才是对象),让对象去解决对应的问题。

# (2)用例子思考

其实我们之前写的代码都是面向过程的,而事实上,我们的大脑处理问题本身就是更加偏向面向对象的。

举一个例子:

你想送你女朋友一个包,

  • 面向对象的思想是,找个卖包包的店,买一个包包。其中不管是商店,还是包都是现实生活中存在的事物,代码里我们称之为对象。
  • 面向过程的思想是:找到原材料,自己切割,自己缝制,每一个工序都自己干,这就是过程。

感觉面向对象忽略了过程一样。

其实,越是高级的语言会越向着人的思考靠近。

  • 面向对象是更高级的抽象,是对现实世界的映射。
  • 思考一下,我们接触过的 String、Scanner就是很好的例子。你看着很简单的字符串,它本身就是个对象,不需要我们自己去完成一个字符一个字符的拼接,Scanner更是牛逼,我们更加不知道它具体是怎么做到让我们从控制台输入的,事实上我们知道它能做什么就足够了。
  • 这就是别人给我们创造的对象,事实上我们也能给自己创造对象,我们也能给别人创造对象。
  • 就像现实中一样,你想吃水果,就去水果摊买,你想按脚,就去足疗店,你想玩,可以去迪斯尼。
  • 当然你也可以开个4s店卖汽车。
  • 没人会关心水果是怎么种的,从哪里来的,按脚的技师是怎么招聘的,迪斯尼是怎么建的,4s店的车是怎么造的。我们关心的只是水果、技师、迪斯尼、汽车这些实实在在的对象而已。

# 2、学习自己造对象

我们准备开个4s店,我们需要车、需要门店对吧,那我们就尝试去搞一个。

  • 先说说我们怎么去用代码描述一辆车:

    定义好多个变量 1、brand 2、color 3、length…

  • 问题又来了,我们怎么描述好几个车?

    [一号车的品牌,二号车的品牌,三号车的品牌…一百号汽车的品牌]

    [一号车的颜色,二号车的颜色,三号车的颜色…一百号汽车的颜色]

我们用了几十个数组去维护一百辆汽车的属性,这简直就是个灾难,数据简直没办法维护,每修改一辆车,必须修改每一个数组。

思考:我们能不能这样去搞呢?

搞一个数组,它就是汽车的数组。

[一号汽车的所有,二号汽车的所有,三号汽车的所有,…一百号汽车的所有]

这样我们一个数组就能维护所有的汽车。

同时,我们已经尝试去面向对象编程了,我们将一个汽车的多个属性尝试进行了打包,这个过程就是在封装对象

# 二、面向对象之封装(encapsulation)

# 1、class和对象

第一步:我们要造车了,必须有个造车的说明书。

第二步:根据说明书,造一百辆车。

其实说明书就是在描述车的具体信息,就是对信息的封装。

public class Car {
    // 汽车的型号
    String brand;
    // 汽车的颜色
    String color;
    // 汽车的长度
    long length;
}

你看,这是不是用汽车的基本信息封装了一个汽车的说明,这叫做类,就是汽车类,一个描述汽车的类。

再看看,我们是怎么根据说明书去构建具体的汽车的,每个具体的汽车我们称之为一个【实例对象】。

public static void main(String[] args) {
    Car car1 = new Car();
    car1.brand = "本田";
    car1.color = "red";
    car1.length = 4120;

    Car car2 = new Car();
    car2.brand = "宝马";
    car2.color = "white";
    car2.length = 5087;

    Car[] cars = {car1,car2};
}

所以明白了吗?

  • Car是,只有一份。
  • car1、car2…car100是根据类构建出来的【实例对象】,可以有很多个。

# 2、多出来的数据类型

划重点了:在这里插入图片描述

1、之前我们学习的八种基础数据类型,这些数据是直接存在栈中的。

2、从今天开始,我们的数据类型就多了,汽车是一种数据类型,大炮是一种数据类型,美女是一种数据类型,所有的类都是数据类型,我们统称为:【引用数据类型】。

此刻起,我们可能才真正的开始了解class这个关键字,他就是用来创建一个类。

像car1、car2、cars这些叫引用,它是指向内存中的一块区域。存放这些实例对象的空间,我们称之为堆。

不妨我们看看,这个【车队】在内存中的结构图:

image-20210720112436796image-20210720112436796

小知识:

类型指针一般为4字节,在关闭压缩普通对象指针时(-XX:+UseCompressedOops)为8字节,UseCompressedOops默认是开启的,只有虚拟机内存达到32G以上,4个字节已经无法满足寻址需求时,才需要关闭该参数。

# 3、成员变量

public class Car {
    // 汽车的型号
    String brand;
    // 汽车的颜色
    String color;
    // 汽车的长度
    double length;
}

成员变量我们已经学过了了,

像汽车型号、颜色、车长等属性,是Car这个类的成员,是每个实例对象都有的属性,我们称之为【成员变量】。

成员变量的赋值:

car2.brand = "宝马";

成员变量在new之后就会有初识值,0,null,false

# 3、成员方法

思考一个问题:

一个汽车如果只有颜色、品牌这些属性,那它就是一块铁。我们买汽车,主要因为汽车能跑啊,可以开着到处跑啊。

很明显,跑是一个动作,他不能用红的,绿的,大的、小的去描述,而是需要一步步去做的。

# (1)定义成员方法

看我怎么给车定义一个方法:

public class Car {
    // 汽车的型号
    String brand;
    // 汽车的颜色
    String color;
    // 汽车的长度
    double length;
    
    public void run(){
        // 中间省略了打火、挂档等动作
        System.out.println(brand+"在飘移!");
    }
}

其中:

public void run(){
    // 中间省略了打火、挂档等动作
    System.out.println(brand+"在飘移!");
}

就是一个成员方法,咱们不妨拆解一下:

  • public:马上学,先不管
  • void:没有返回值。
  • run:方法的名字。
  • ():内部可以传入参数。

# (2)参数

汽车要跑是不是要加油啊!加92号和95号油可能效果不一样。

可以这样改造: gasoline [ˈɡæsəliːn]

// 定义方法,这里的gasoline是形参,形式上的参数
public void run(int gasoline){
    System.out.printf("您加了%d号汽油",gasoline);
    if(gasoline == 92){
        System.out.println("92号汽油跑的很快!");
    } else if(gasoline == 95){
        System.out.println("95号汽油跑的更猛!");
    } else {
        System.out.println("你加了柴油吧!");
    }
}

怎么调用啊?

public static void main(String[] args) {
    Car car1 = new Car();
    car1.brand = "本田";
    car1.color = "red";
    car1.length = 4.12;
    // 方法调用,这里的95是实参
    car1.run(95);
}

参数可以有很多个。可以用逗号隔开。

public void run(int arg1,String arg2,long arg3);

# (3)返回值

void:代表没有返回值

返回值是一个方法执行完毕,想要告诉你的信息。

比如我们要发动汽车让他跑,但是它具体有没有跑起来可能是个问题,可能因为年久失修坏掉了。

// 定义一个run方法,生成一个随机数,车有一定的概率坏掉了
public boolean run(){
    // 这句代码能生成一个0~1的double的数字
    double random = Math.random();
    if(random > 0.5){
        System.out.println("车子正常启动!");
        return true;
    } else {
        System.out.println("车子坏了!");
        return false;
    }
}

// 调用方法
car1.run();

方法执行完成之后,它会告诉我们一个布尔值,代表车子是不是坏了,我们可以【用一个变量去接收它】。

boolean canRun = car1.run();
System.out.println(canRun);

# (4)return关键字

其中return有两层含义:

  1. 终止当前方法继续执行。
  2. 返回方法的返回值。

思考一个题目:

在void中,即没有返回值得方法中能 用return吗?

答案是可以的,这里return只能代表方法的终止。

思考下边的代码,结果是什么?

// 没有return,最后的打印一定会执行
public void run(int gasoline){
    System.out.printf("您加了%d号汽油",gasoline);
    if(gasoline == 92){
        System.out.println("92号汽油跑的很快!");
    } 
    if(gasoline == 95){
        System.out.println("95号汽油跑的更猛!");
    } 

    System.out.println("你加了柴油吧!");
}

// 遇到return,最后的打印就不执行
public void run(int gasoline){
    System.out.printf("您加了%d号汽油",gasoline);
    if(gasoline == 92){
        System.out.println("92号汽油跑的很快!");
        return;
    } 
    if(gasoline == 95){
        System.out.println("95号汽油跑的更猛!");
        return;
    } 

    System.out.println("你加了柴油吧!");
}

# (5)方法的递归

# a、方法调用

方法时可以调用的,一个方法中也是可以调用另一个方法的。

我们完全可以把加油和发动分解成两个动作啊,理论上,这也是合理的,因为这确实是两个动作。

// 发动的方法
public void run(){
    addGasoline();
    System.out.println("汽车启动啦!");
}
// 加油的方法
public void addGasoline(){
    System.out.println("加油啦!");
} 
# b、递归调用

问题来了,方法

方法自己能不能够调用自己的方法

// 发动的方法
public void run(){
    run();
    System.out.println("汽车启动啦!");
}

这玩意直接报错了:

Exception in thread "main" java.lang.StackOverflowError
	at com.ydlclass.Car.run(Car.java:16)
	at com.ydlclass.Car.run(Car.java:16)
	at com.ydlclass.Car.run(Car.java:16)

说是栈内存溢出了:

什么原因呢?每个方法的创建都会创建一个【栈帧】压入栈中。

image-20210720163640565image-20210720163640565

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

所以,在使用递归的时候一定要注意,用不好,会发生栈内存溢出的问题。

那怎么用好递归呢?

答案是:在合适的地方退出递归调用,接下来举两个例子。

# c、斐波那切数列

斐波那契数列指的是这样一个数列 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,377,610,987,1597,2584,4181,6765,10946,17711,28657,46368……

特别指出:第0项是0,第1项是第一个1。

这个数列从第三项开始,每一项都等于前两项之和。

求:在斐波那契数列中第 number个数字是多少?

分析:我们知道 除了第0个和第1个,【第number个数字】一定等于 【第number-1个数字】和 【第number-2个数字】之和。

public long fibonacci(long number) {
    return fibonacci(number - 1) + fibonacci(number - 2);
}

上边这个递归永远退不出去,应该判断number在0和1的时候,它并不需要递归,修改如下:

public long fibonacci(long number) {
    if ((number == 0) || (number == 1))
        return number;
    else
        return fibonacci(number - 1) + fibonacci(number - 2);
}


Test test = new Test();
long result = test.fibonacci(5);
System.out.println(result);
结果:
   5

【注意】:递归,一定要有合理的退出机制。

# d、阶乘(factorial)

5的阶乘 = 54321 = 120

非递归方式:

public long factorial(long number) {
    long result = 1;
    for (int i = 1; i <= number ; i++) {
        result *= i;
    }
    return result;
}

递归方式:

核心思路:5的阶乘 = 4的阶乘 * 5 = 3的阶乘 * 4 * 5 = 2的阶乘 * 3 * 4 * 5 = 1 * 2 * 3 * 4 *5;

public long factorial(long number) {
    if (number <= 1)
        return 1;
    return number * factorial(number - 1);
}

# (6)方法的重载

在java 中允许同一个类中, 多个同名方法的存在, 但要求【形参列表】不一致!这个和【返回值】无关。

重载方法能让我们更好更方便的起名字。

//两个整数的和
plus(int n1, int n2)           
//一个整数, 一个 double 的和
plus(int n1, double n2)     
//一个 double ,一个 Int 和
plus(double n2, int n1)     
//三个 int 的和
plus(int n1, int n2,int n3)    

构成重载的三个要素:

  • 方法名必须一致。
  • 参数不一致,有两层含义第一是参数的数量不一致,第二层含义是:参数的类型不一致,【参数的名字一样不一样都行】。
  • 返回值无要求。

我们确实有这种需求,都是加法,但是需要的参数不同,我们有必要去为了它而创建一个新的方法名字吗?

plus1,plus2,plus3… 当然这样做也没有错,但是使用重载会让你的代码更加的优雅一点。

思考:

有这样一个方法:public void fun(int a,int b);下边哪些方法和它重载。

- public void fun(int x,int y);              //不是
- public void fun(int x,int y,int z);        //是
- public void fun(int a,int b,int c);        //是
- public int fun(int a,int b);               //不是
- public int fun(int a,int b,int c);         //是

【作业】

写两个重载的方法max()

  • 一个方法求两个数的最大值。
  • 一个方法求三个数的最大值,思路:先求出第一个和第二的最大值,再拿最大值和第三个比较。

# (7)可变参数

有的时候我们是否有这种需求,我们想求几个数之和。

我们想写成 plus(1,2,3,4,....) 这个样子。

java中还真的有,这种参数可以随心所欲去传递的参数叫做【可变参数】。

【语法】:访问修饰符 返回类型 方法名(数据类型… 形参名) {}

举个例子:

//1.int...表示接受的是可变参数,类型是int,即可以接收多个int(0-多)
//2.使用可变参数时,可以当做数组来使用即nums可以当做数组
//3.遍历nums求和即可
public int sum(int... nums){
    int result=0;
    // 可变参的本质其实是个数组
    for(inti=0;i<nums.length;i++){
        result += nums[i];
    }
    return result;
}

你一旦定义了可变参数,它居然可以这么随心所欲的调用:

xxx.sum(1);
xxx.sum(1,2);
xxx.sum(1,32,3);

你完全没必要为每一种求和方法提供一个重载的方法。

本质:

就是将你传入的参数封装成了一个数组,他和可变参数是一样的,只是数组你需要自己去定义。

public int sum(int[] nums){
    int result=0;
    // 可变参的本质其实是个数组
    for(inti=0;i<nums.length;i++){
        result += nums[i];
    }
    return result;
}

注意点:

一个方法的形参列表最多只能出现一个可变参数。

public int sum(int... nums,int... nums2);  // 不可以

可变参数可以和普通参数放在一起,但是可变参数必须放在最后。

public int sum(int first,int... nums2);   // 可以
public int sum(int... nums2,int last);    // 不可以

# (8)局部变量和作用域

之前讲了,定义在类中的变量叫成员变量,那么定义在成员方法中的变量就局部变量。

  • 成员变量会有默认值:基础数据都是零,char中的零是代表空字符,boolean是false,引用数据类型都是null;
  • 局部变量没有默认值:必须初始化才能使用。

# 4、权限修饰符

# (1)多个类相互调用

目前为止,我们并没有在第二个类中去调用另一个类的内容:

来跟我写:

创建一个Test类,在Test类中:

public class Test {
    public static void main(String[] args) {
        Car car = new Car();
        car.run();
    }
}

我们发现了一个很神奇的现象,在另一个类中居然也可以new一个Car。

我们之前的项目都是单打独斗,把所有的代码都写在了同一个类里边,将来我们的代码可能成千上万行,真的写在一个文件当中吗?

将来,我们的项目都是工程化的,合理利用每一个文件显得尤其重要。

# (2)包

这个【包】不是你女朋友背的包包。

在我们工程化的项目当中,我们可能有成千上万个类?

我们不去讨论类,假如给你一千张照片,让你存在电脑中,你也不会傻呼呼的全部存在一个文件中吧!

最起码会给照片分一下类吧

image-20210721141836211image-20210721141836211

分类有什么好处啊:

  • 我们可以很快的检索一个图片,因为一个文件中的图片都有特定的特征。
  • 我们给照片文件起名字时甚至可以重名,只要相同名字的文件存放在不同的目录即可。
c:\照片\人物\1.jpg
c:\照片\风景\1.jpg

其实包也是一样,本质上就是一个文件夹,用来归纳整理我们的类

下面就是工程化的项目的包结构,这些文件夹下边装的就是咱们的类,只不过进行了压缩,成了jar文件:

image-20210721142444394image-20210721142444394

下边:

image-20210721142748538

来,咱们看看我们经常用的那几个类究竟在哪里?

image-20210721143804275

其实:java本身给我们提供了很多拿来即用的类,比如String,

他所在的包叫 java.lang 这是一个很特殊的包。

除了jdk的包,我们可能还会用到其他公司程序员编写的类,那怎么保证我们用了多家公司写的类,还不重名呢?

类是程序员写的,我们没办法控制,所以只能在包上下功夫,尽量让每个公司的包都不一样。

有人就很聪明,使用 域名倒置 的方法给包命名,因为每个公司的域名都不一样。

为什么是域名倒置呢?举一个例子:

zhidao.baidu.com
wenku.baidu.com
map.baidu.com

com.baidu.zhidao
com.baidu.wenku
com.baidu.map

本质上,每一个点代表一个文件夹。哪一种方式更适合咱们建立文件夹呢?

  • 第一种方式:先建立三个文件夹,每个文件夹下baidu百度文件夹,里边简历com文件夹
  • 第二种方式:建立com文件夹,里边建立baidu文件夹,里边建立三个文件夹

使用域名做包的好处

  • 引入其他人写的类的时候保证不重名。
  • 一眼就能看出是哪个公司的作品。

怎么导入一个包

引入单个类:import com.yxlclass.Car;

引入包下的所有类:import com.yxlclass.*;

什么情况不需要导入

  • 但是你有没有发现,我们使用String的时候从来没有使用 import,因为这个包会默认导入
  • 相同的包下不需要使用 import 显示的引入。

一个类的全类名

一个类的类名现在该怎么表示?

全类名:com.ydlclass.Car

# (3)权限修饰符

  • 我们除了可以按照自己的想法,封装世界的万事万物,封装还给我们提供了更加丰富的内容。
  • 我们可以按照我们的需求,对封装在对象内的属性和方法提供不同的权限,刻意暴露或隐藏的具体的实现的细节。

这个就要依靠权限修饰符来完成了,其实我们已经见过很多次了:

作用域当前类同package子孙类其他package
public
protected×
friendly( default )××
private×××

只能用来修饰成员变量,局部变量不能,friendly在子孙类中不能调用是指的跨包(继承和被继承的类不在同一个包).

权限修饰符修饰方法和属性 效果一样:

public String name;
private int age;
protected String email;
String password;

protected void a(){
}
private void b(){
}
public void c(){
}
void d(){
}

class只能被public修饰,但是内部类可以被以上几种修饰

public class Dog {
    protected class A{
    }
    private class B{
    }
    public class C{
    }
    class D{
    }
}

内部类,我们会有专门时间讲。

局部变量不能使用权限修饰符.

# 5、构造器

我们一直在学习怎么封装class,一个class封装好之后的目的一般是创建实例对象,我们其实已经做过了。

Dog dog = new Dog();

new 一个对象的时候到底发生了什么事情呢?

  1. java在new一个对象的时候,会先查看对象所属的类有没有被加载到内存,如果没有的话,就会先通过类的权限定名来加载,这个过程我们后边聊。
  2. 加载并初始化类完成后,再进行对象的创建工作。

我们先聊聊创建的过程:

  1. 在堆区分配对象需要的内存。
  2. 对所有实例变量赋默认值。
  3. 执行构造方法,比如我们可以使用构造方法赋值。(不太准确,以后会慢慢补充)
  4. 在栈区定义引用变量,然后将堆区对象的地址赋值给它。

image-20210813142508209image-20210813142508209

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

构造方法,也叫构造器,会在new对象的时候主动被调用。

但是,事实上,我们并没有看到任何构造方法。

每个java类,会默认送你一个构造方法,这个方法它是这个样子的:

public Dog(){        
}

我们可以写出来,也可以不写,这个构造方法什么都没做,我们可以想办法让它做一些事情,比如:

public Dog(){
    name = "teddy";
    age = 12;
}

public static void main(String[] args) {
    Dog dog = new Dog();
    System.out.println(dog.name);
    System.out.println(dog.age);
}

我们并没有调用这个方法啊:
但是,结果却出来了:
    teddy
    12

当然如果我们想自己按照自己的意愿去构造实例,我们还可以定义这样的构造器:

同时new的时候就要传递参数了:

public Dog(String name,int age){
    System.out.println("验证构造方法被调用的时机:【"+ name + "】被创建!");
    this.name = name;
    this.age = age;
}

public static void main(String[] args) {
    Dog dog = new Dog("jinmao",23);
    System.out.println(dog.name);
    System.out.println(dog.age);
}

结果:
    验证构造方法被调用的时机:【jinmao】被创建!
    jinmao
	23

此时,如果我们把那个没有参数的构造器删除,

你会发现已经不能这么去new对象了。

image-20210721170629934image-20210721170629934

【注】一旦自己写了构造器,系统将不再赠送,所以我们一般【会自己补充上】。

# 6、this关键字

  • 记住一点:每一个方法都会默认传入一个变量叫this,它永远指向调用它的【当前实例】。

image-20210813143046989image-20210813143046989

# (1)this访问属性和方法

写段代码:

//构造方法和其他方法一样可以重载,可以有参数,名字必须和类名一样,不能有一点区别
public Dog(String name){
    System.out.println("验证构造方法被调用的时机:【"+ name + "】被创建!");
    this.name = name;
}

public void eat(){
    // this也可以在成员方法中使用
    System.out.printf("【%s】再吃骨头。",this.name);
}

public static void main(String[] args) {
    //直接new对象看看new的时候是不是调用了构造方法,事实证明是的
    new Dog("哈士奇").eat();
}

一个方法只有在调用的时候,才能明确方法中的【this】具体指向哪个实例对象。

我们可以使用this访问当前对象的方法和属性。

# (2)this访问构造器

还可以访问当前类的构造器:

//构造方法和其他方法一样可以重载,可以有参数,名字必须和类名一样,不能有一点区别
public Dog(String name){
    System.out.println("验证构造方法被调用的时机:【"+ name + "】被创建!");
    this.name = name;
}

public Dog(){
    this("default");
}

如果我们使用无参构造会传入一个默认值,这就是典型的案例

this当做构造器只能放在第一行

# 7、setter和getter

我们之前调用一个实例对象的属性的时候,都是 dog.name 但事实上,java程序员从来不会这么干。

我们有这样的规定,所有的属性必须私有化,使用setter和getter赋值或者取值,

public class Dog {
    
    //有哪些特点
    //定义狗有颜色这个属性
    private String color;
    //定义狗有种类这个属性
    private String type;
    //定义狗有年龄这个属性
    private int age;
    
    //java约定使用setter和getter方法进行属性的取值和赋值
    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
    //..狗还有很多属性和方法,我们无法一一列举
}

为什么呢?

  • getter方法能够按照客户的期望返回格式化数据。
  • setter方法可以限制和检验setter方法传入的参数,隐藏对象内部数据结构。
  • 属性不具备多态性。

举个例子:

public class Girl {
    
    private int age;

    public int getAge() {
        // 你问我多大了,真实年龄大于18岁,都是18岁
        if(this.age > 18){
            return 18;
        }
        return age;
    }

    public void setAge(int age) {
        // 每过一年,如果我超过了18岁,我永远是18岁。
        if(age > 18){
            this.age = 18;
        }
        this.age = age;
    }
}

所以,正确的定义一个类,一定是

  • 所有的属性私有化。
  • 每个属性都有对应的getter和setter放。

这是规矩,你得遵守。

# 8、String详解

字符串是引用类型,但是为什么不用new,因为太常用了,就简化了。

如果你不觉得烦,也能写成:

String name = new String("name");

String name = "name";   就行了

既然是个对象就有属性和方法

它的方法无非就是帮助我们方便的处理这个字符串。

注:使用string一定要注意,必须用一个新的String接受。

String substring = name.substring(1, 3);

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

# (1)符串查找

String 类的 indexOf() 方法在字符串中查找子字符串出现的位置,如过存在返回字符串出现的位置(第一位为0),如果不存在返回 -1。

public class SearchStringEmp {
   public static void main(String[] args) {
      String strOrig = "xinzhi bigdata Java";
      int intIndex = strOrig.indexOf("Java");
      if(intIndex == - 1){
         System.out.println("没有找到字符串 Java");
      }else{
         System.out.println("Java 字符串位置 " + intIndex);
      }
   }
}

也可以用contains() 方法

# (2)字符串替换

java String 类的 replace 方法可以替换字符串中的字符。

public class test {
    public static void main(String args[]){
            String str="Hello World,Hello Java.";
            System.out.println(str.replace('H','W')); //替换全部
            System.out.println(str.replaceFirst("He","Wa")); //替换第一个遇到的
            System.out.println(str.replaceAll("He", "Ha")); //替换全部
       }
}

# (3)字符串分割

split(string) 方法通过指定分隔符将字符串分割为数组。

public class test {
    public static void main(String args[]){
            String str="www-baidu-com";
            String delimeter = "-";  //指定分隔符
            String[] temp = str.split(delimeter);  //分割字符串
            //普通for循环
            for(int i =0; i < temp.length; i++){
                System.out.println(temp[i]);
                System.out.println("");
            }
            
            System.out.println("----java for each循环输出的方法-----");
            String str1 = "www.baidu.com";
            String delimeter1 = "\\.";   //指定分隔符,.号需要转义,不会明天讲
            String[] temp1 = str1.split(delimeter1);
            for (String x : temp1){
                System.out.println(x);
                System.out.println("");
            }    
       }
}

# (4)字符串截串

substring(string) 方法可以截取从第几个下标(0开始,包含第一个开始)到第几个下标(不包含)的字符串。

public class test {
    public static void main(String args[]){
        String name = new String("name");
        String substring = name.substring(1, 3);
    }
}

# (5)字符串小写转大写

String toUpperCase() 方法将字符串从小写转为大写。

String str = "string runoob";
String strUpper = str.toUpperCase();

作业:

查找某个单词在文章中出现的次数:

public static void main(String[] args) {
    String str = "Hello World abc Hello";
    // 截取字符串 第一个包含的 第二个不包含
    Test2 test2 = new Test2();
    int count = test2.wordCount(str, "HeLlo");
    System.out.println(count);

}

public int wordCount(String article, String word){
    //1、先把文章打散成数组
    String[] words = article.split(" ");
    int res = 0;
    for (int i = 0; i < words.length; i++) {
        if(words[i].equalsIgnoreCase(word)){
            res++;
        }
    }
    return  res;
}

# 9、包装类和自动拆装箱

有时候我们相对基础数据类型进行一些操作,但因为基础类型没有方法,不好操作。

其实java对每一种基础类型都进行了封装,生成对应的包装类

基本数据类型包装类
byteByte
booleanBoolean
shortShort
charCharacter
intInteger
longLong
floatFloat
doubleDouble

Integer是个对象,本来是要new的。

但是太常用了,所以简化了定义的方式,和基础类型一样。

// 本来是要这么写的:
Integer i= new Integer(3);
// 事实上,我们这么写也行
Integer i= 3

这很明显,左边是包装类,右边是基础数据类型,这种静默的转化我们称之为自动拆装箱。

  • 自动装箱:将基础数据类型自动装换为包装类。
  • 自动拆箱:将包装类自动转换为基础数据类型。
// 自动装箱
Integer i = 10;
// 自动拆箱
int m = i;
public static void main(String[] args) {
    Integer num2 = 127;
    Integer num1 = 127;

    System.out.println(num1 == num2);
}
true

public static void main(String[] args) {
    Integer num2 = 128;
    Integer num1 = 128;

    System.out.println(num1 == num2);
}
false
    
public static void main(String[] args) {
    Integer num2 = new Integer(127);
    Integer num1 = new Integer(127);

    System.out.println(num1 == num2);
}
false

image-20210720193157583

在Integer的源码中,有个缓存,缓存了 -128~127的Integer对象。

image-20210720192927951image-20210720192927951

我想问问: num1 == num2 到底比的是什么?

# 10、封装一个超级数据

问自己一个问题,数组好用吗?数组好用吗?

使用数组进行增删查改,简直太麻烦了,我们既然学习了封装,那为什么不去封装一个好用的数组呢?

暂且称之为:超级数组,SuperArray

package com.ydlclass;

public class SuperArray {

    //维护一个数组,要想什么都存,就要使用顶级父类
    private Integer[] array;

    // 数组的长度
    private int size;

    // 数组当前的容量
    private int capacity;

    //构造是初始化
    public SuperArray(){
        this(10);
    }

    //构造是初始化
    public SuperArray(int capacity){
        array = new Integer[capacity];
        this.capacity = capacity;
    }

    //添加数据的方法,默认是尾插
    public boolean add(Integer data){
        // 确保容量足够,如果容量不够就扩容
        ensureCapacity(size + 1);
        array[size++] = data;
        return true;
    }

    //添加数据的方法,指定下标添加
    public void add(int index,Integer data){

        ensureCapacity(size + 1);

        /**
         * Object src : 原数组
         * int srcPos : 从元数据的起始位置开始
         * Object dest : 目标数组
         * int destPos : 目标数组的开始起始位置
         * int length  : 要copy的数组的长度
         */
        // index以后的数据统一向后移动,空出位置
        System.arraycopy(array, index, array, index + 1,
                size - index);

        array[index] = data;
        size++;
    }

    public Integer remove() {
        if(size > 0){
            return array[--size];
        }
        return null;
    }

    public Integer remove(int index) {
        boolean flag = rangeCheck(index);
        if(flag){
            Integer res = array[index];
            /**
             * Object src : 原数组
             * int srcPos : 从元数据的起始位置开始
             * Object dest : 目标数组
             * int destPos : 目标数组的开始起始位置
             * int length  : 要copy的数组的长度
             */
            System.arraycopy(array, index+1, array, index, size-index);
            size--;
            return res;
        }
        return null;
    }

    public boolean set(int index,Integer data) {
        boolean flag = rangeCheck(index);
        if(flag){
            array[index] = data;
            return true;
        }
        return false;
    }


    //根据下标查询数字
    public Integer get(int index){
        boolean flag = rangeCheck(index);
        if(flag){
            return array[index];
        }
        return null;
    }

    //查看当前有多少个数字
    public int size(){
        return this.size;
    }

    // 确保容量,并在需要的时候扩容
    private void ensureCapacity(int needCapacity) {
        System.out.println(needCapacity + "---" + capacity);
        if (needCapacity > capacity){
            // 算一算,这个大概扩容了多少倍
            capacity = capacity + (capacity >> 1);
            Integer[] newArray = new Integer[capacity];
            /**
             * Object src : 原数组
             * int srcPos : 从元数据的起始位置开始
             * Object dest : 目标数组
             * int destPos : 目标数组的开始起始位置
             * int length  : 要copy的数组的长度
             */
            System.arraycopy(array, 0, newArray, 0, array.length);
            array = newArray;
        }
    }

    //验证下标是否合法
    private boolean rangeCheck(int index) {
        //只要有一个不满足就返回false
        return index <= size-1 && index >= 0;
    }

    public static void main(String[] args) {
        SuperArray superArray = new SuperArray();
        superArray.add(1);
        superArray.add(2);
        superArray.add(3);
        superArray.add(4);
        superArray.add(5);
        superArray.add(5);
        superArray.remove(3);
        superArray.remove();
        superArray.set(2,45);
        for (int i = 0; i < superArray.size(); i++) {
            System.out.println(superArray.get(i));
        }
    }
}

思考:

为什么有的方法是私有的,有的方法是公有的,size属性不去私有化会不会出问题。

# 11、封装一个超级链表

  • 又是一个新的名词:链表
  • 在内存空间中,数组和链表都是基本的数据结构,都是【表】,或者叫【线性表】。
  • 线性表是一个线性结构,它是一个含有n≥0个结点的有限序列,对于其中的结点,有且仅有一个开始结点没有前驱但有一个后继结点,有且仅有一个终端结点没有后继但有一个前驱结点,其它的结点都有且仅有一个前驱和一个后继结点,说人话,就是有头有尾一条线。

image-20210722113751650image-20210722113751650

还不明白就在来一张图:

image-20210722114252568image-20210722114252568

代码:

1、写链表首先要封装一个保存数据和引用的节点,我们俗称node

public class Node {
    private Integer data; 
    private Node next;

    public Integer getData() {
        return data;
    }

    public void setData(Integer data) {
        this.data = data;
    }

    public Node getNext() {
        return next;
    }

    public void setNext(Node next) {
        this.next = next;
    }
}
package com.ydlclass;

public class SuperLinked {

    // 链表的长度
    private int size;
    // 维护一个头节点
    private Node first;
    // 维护一个尾节点
    private Node last;

    // 无参构造器
    public SuperLinked(){

    }

    //添加元素至链表尾部
    public boolean add(Integer data){
        Node node = new Node(data,null);
        if (first == null){
            first = node;
        } else {
            last.setNext(node);
        }
        last = node;
        size++;
        return true;
    }

    //在指定下标添加元素
    public boolean add(int index,Integer data){
        Node node = getNode(index);
        Node newNode = new Node(data,null);

        if (node != null){
            newNode.setNext(node.getNext());
            node.setNext(newNode);
        } else {
            first = newNode;
            last = newNode;
        }

        size++;
        return true;
    }

    // 删除头元素
    public boolean remove(){
        if (size < 0){
            return false;
        }

        if (first != null ){
            first = first.getNext();
            size--;
        }
        return true;
    }

    // 删除指定元素
    public boolean remove(int index){
        if (size < 0){
            return false;
        }
        if(size == 1){
            first = null;
            last = null;
        } else {
            Node node = getNode(index-1);
            node.setNext(node.getNext().getNext());
        }
        size--;
        return true;
    }

    // 修改指定下标的元素
    public boolean set(int index,Integer data){
        // 找到第index个
        Node node = getNode(index);
        node.setData(data);
        return true;
    }

    // 获取指定下标的元素
    public Integer get(int index){
        return getNode(index).getData();
    }

    //查看当前有多少个数字
    public int size(){
        return size;
    }

    //添加元素
    private Node getNode(int index){

        // 边界判断
        if(index <= 0){
            index = 0;
        }
        if(index >= size-1){
            index = size-1;
        }

        // 找到第index个
        Node cursor = first;
        for (int i = 0; i < index; i++) {
            cursor = cursor.getNext();
        }
        return cursor;
    }

    public static void main(String[] args) {
        SuperLinked linked = new SuperLinked();
        linked.add(1);
        linked.add(2);
        linked.add(4);
        linked.add(6);
        linked.add(3);
        linked.add(2);
        linked.add(7);
        linked.add(6);
        linked.remove();
        linked.remove(2);
        linked.set(0,3);
        for (int i = 0; i < linked.size(); i++) {
            System.out.println(linked.get(i));
        }
    }
}

# 12、封装一个栈和队列

栈(Stack)和队列(Queue)是两种操作受限的线性表。

这种受限表现在:栈的插入和删除操作只允许在表的尾端进行(在栈中成为“栈顶”),满足“FILO:First In Last Out”;队列只允许在表尾插入数据元素,在表头删除数据元素,满足“First In First Out”。

栈与队列的相同点:

  1. 都是线性结构。
  2. 插入操作都是限定在表尾进行。
  3. 都可以通过顺序结构和链式结构实现。、

栈与队列的不同点:

  1. 队列先进先出,栈先进后出。

队列

package com.ydlclass;


public class Queue {

    private SuperLinked superLinked = new SuperLinked();

    // 出队的方法
    public Integer poll(){
        if(empty()){
            return null;
        }
        Integer integer = superLinked.get(0);
        superLinked.remove();
        return integer;
    }
    
    // 返回队首,不出队
    public Integer peek(){
        if(empty()){
            return null;
        }
        return superLinked.get(0);
    }

    // 入队的方法
    public void add(Integer item){
        superLinked.add(item);
    }

    // 判断这个队列是否为空
    public boolean empty(){
        return superLinked.size() == 0;
    }

    public static void main(String[] args) {
        Queue queue = new Queue();
        queue.add(1);
        queue.add(2);
        queue.add(3);

        System.out.println(queue.poll());
        System.out.println(queue.poll());
        System.out.println(queue.poll());
        System.out.println(queue.poll());
        System.out.println(queue.poll());
    }

}

package com.ydlclass;

public class Stack {

    private SuperLinked superLinked = new SuperLinked();

    // 弹出栈顶,并且返回
    public Integer pop(){
        if(empty()){
            return null;
        }
        Integer integer = superLinked.get(superLinked.size() - 1);
        superLinked.remove(superLinked.size() - 1);
        return integer;
    }

    // 返回栈顶元素,不弹栈
    public Integer peek(){
        if(empty()){
            return null;
        }
        return superLinked.get(superLinked.size() - 1);
    }

    // 压栈方法
    public void push(Integer item){
        superLinked.add(item);
    }

    // 判断这个队列是否为空
    public boolean empty(){
        return superLinked.size() == 0;
    }

    public static void main(String[] args) {
        Stack queue = new Stack();
        queue.push(1);
        queue.push(2);
        queue.push(3);

        System.out.println(queue.pop());
        System.out.println(queue.pop());
        System.out.println(queue.pop());
        System.out.println(queue.pop());
        System.out.println(queue.pop());
    }

}

# 13、银行取票机

package com.ydlclass;

import java.util.Scanner;

/**
 * @author itnanls
 * @date 2021/7/22
 **/
public class BankTicketMachine {

    private Queue queue = new Queue();

    private int startNumber = 100;

    private Scanner scanner = new Scanner(System.in);

    public void pushTicket(int ticketNumber){
        for (int i = 0; i < ticketNumber; i++) {
            startNumber += i;
            queue.add(startNumber);
        }
    }

    public Integer getTicket(){
        if(queue.empty()){
            System.out.println("号码已经被全部领取,需要继续释放号码!");
            System.out.println("请输入释放号码的个数:");
            Integer number = scanner.nextInt();
            pushTicket(number);

        }
        return queue.poll();
    }

    public void run(){
        while (true){
            System.out.println("请输入您的名字:");
            String name = scanner.next();
            Integer ticket = getTicket();
            System.out.println("尊敬的【"+name + "】,您的号码是:" + ticket + "。");
        }
    }

    public static void main(String[] args) {
        BankTicketMachine machine = new BankTicketMachine();
        machine.run();
    }

}

image-20210722144348696image-20210722144348696

# 三、面向对象之继承(Inheritance)

提出需求来思考:

我想创建一个学生类,男学生类,女学生类,会有这么几个问题:

1、不管是男同学还是女学生,都是学生,学生公有的方法和属性本来就有很多。

2、虽然都是学生,但是男女毕竟有别,还是有一些不一样的地方。

在以往的认知当中,我们不得不创建学生类,男学生类,女学生类,然后书写每一个重复的属性和方法。

但是java给我们提供了更好的解决方案叫继承。

# 1、基本介绍

继承可以解决代码复用的问题,一个类可以继承一个类,被继承的类我们称之为【父类】或者【超类】,另一个类称之为【子类】也叫【派生类】,子类可以通过extends关键字轻松拥有获取父类的成员变量和成员方法的能力,除了被private修饰的。在java中是单继承的,这样可以规范代码的实现。

继承其实很好理解的,我们天生就会继承来自父母的很多基因,那爸爸的很多能力或者特征你天生就会拥有。

image-20210803142249085image-20210803142249085

写一个小代码,我们尝试理解一下:

// 定义一个父亲类,有名字属性和一个吃的方法
public class Father {
    private String name;
    
    public void eat(){
        System.out.println("I am eating!");
    }
    
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

// 子类使用extends关键字
public class SonONe extends Father {
}

// 子类使用extends关键字
public class SonTwo extends Father {
}

现在无论是哪个子类都能调用父类的方法:

public static void main(String[] args) {
    SonONe  sonONe = new SonONe();
    sonONe.eat();
}

当然,儿子作为一个单独的个体,它依然可以拥有属于自己的属性和方法。

public class SonONe extends Father {
    public void play(){
        System.out.println("I am playing!");
    }
}

经过这样的设计,我们的代码实现起来可以十分的灵活。

比如:我们现在设计一款游戏,英雄者小游戏(王者荣耀),我们是不是就可以这样设计类了。

image-20210803164839552image-20210803164839552

代码我们学完了面向对象后尝试写:

public class Father {
    public String name = "lily";

    public Father() {
        System.out.println("Father is created!");
    }

    public void eat(){
        System.out.println("father is eating!");
    }
}
public class Son extends Father{

    public Son() {
        System.out.println("Son is created!");
    }

    public void work(){
        System.out.println("son is working!");;
    }

}
public class GrandSon extends Son{

    public GrandSon() {
        System.out.println("GrandSon is created!");
    }

    public void play(){
        System.out.println("grandson is playing!");
    }

}
public static void main(String[] args) {
        new GrandSon();
    }
    
    
Father is created!
Son is created!
GrandSon is created!

创建子类前一定会先创建他的父类.

# 2、super关键字

super代表指向父类实例的引用

这里问题就来了,我们new了子类,又没有new父类,怎么就有了父类的实例了呢?

在之前的课程中,我们介绍了,在方法中我们可以使用this关键字指向调用该方法的实例对象,同样方法中还用一个super关键字他指向父类的实例对象。

那问题来了,我只是new了一个子类,哪里来的父类对象呢?

由此,我们推算出,构造一个子类一定会先构造一个父类,不服咱们上例子:

我们都知道,一个类被构造之后,会主动的调用它的构造方法,我们可以来试试:

public class Father {
    public Father(){
        System.out.println("父类被构造了!");
    }
}

public class SonONe extends Father {
    public SonONe(){
        System.out.println("子类被构造了!");
    }

    public static void main(String[] args) {
        SonONe sonONe = new SonONe();
    }
}


结果:
父类被构造了!
子类被构造了!

结论很明显,父类确实被构造了。

它的作用有以下几个:

  • 在子类的成员方法中,访问父类的成员变量。
  • 在子类的成员方法中,访问父类的成员方法。
  • 在子类的构造方法中,访问父类的构造方法。

# (1)使用super调用父类的方法和属性

在子类中调用父类的非私有属性和方法时,大致的过程如下:

  1. 先在当前类中寻找。
  2. 当前类没有,继续向父类中寻找。
  3. 如果还是没有,就向父类的父类继续寻找。
  4. 直到到达一个所有类的共同父类,他叫Object。

那么问题来了,我想使用父类的属性,直接用就行了,super有啥用啊,那如果子类也定义了相同名字的属性呢?

例子:

public class Father {
    public String name = "father";
}

public class SonONe extends Father {

    public String name = "son";

    public void printFatherName(){
        System.out.println(super.name);
    }

    public void printMyName(){
        System.out.println(this.name);
    }

    public static void main(String[] args) {
        SonONe sonONe = new SonONe();
        sonONe.printFatherName();
        sonONe.printMyName();
    }

}

结果:
father
son

试想一下,如果没有super,我们是不是真的没有办法访问父类的名字了。

# (2)在子类的构造方法中,访问父类的构造方法

// 定义一个父类
public class Father {
    private String name;

    public Father(){
    }

    public Father(String name){
        this.name = "father-"+name;
    }

    public String getName() {
        return name;
    }
}

// 定义一个子类
public class SonONe extends Father {

    public String name = "son";

    public SonONe() {
    }

    public SonONe(String name) {
    }

    public static void main(String[] args) {
        SonONe sonONe = new SonONe();
        System.out.println(sonONe.getName());
    }

}

结果:
    null

很明显:子类在构造的时候只会默认调用父类的【空参构造】

这里如果我们有一个需求:

子类要通过父类的有参构造,又该怎么办呢?

public class SonONe extends Father {

    public String name = "son";

    public SonONe() {
        super("default");
    }

    public SonONe(String name) {
        super(name);
    }

    public static void main(String[] args) {
        SonONe sonONe = new SonONe();
        System.out.println(sonONe.getName());
    }
}

结果:
father-default

我们成功的调用了父类的构造器。

构造先行:

我们发现,当我们把任何代码放在super之前,编译都会出错:

image-20210803172816162image-20210803172816162

其实很好理解,父类还没有构造,你的代码凭什么执行?

所以:【super构造器只能放在第一行】

而this关键字也只能放第一行,不好意思这两个只能选一个。

【总结一下】

  1. 子类继承了父类所有的非私有的属性和方法,可以直接调用。
  2. 子类在构造的时候,一定会构造一个父类,默认调用父类的无参构造器。
  3. 子类如果希望指定去调用父类的某个构造器, 则显式的调用一下 : super(参数列表)
  4. super和this当做构造器使用时, 必须放在构造器第一行,所以只能二选一。
  5. java 所有类都是 Object 类的子类, Object 是所有类的基类.
  6. 子类最多只能继承一个父类(指直接继承), java 中是单继承机制,我们可以使用连续继承来实现。
thissuper
访问属性访问本实例的属性,没有会继续向父类检索访问父类实例的属性,没有会继续向父类检索
调用方法访问本实例的方法,没有会继续向父类检索访问父类实例的方法,没有会继续向父类检索
调用构造器调用本类的构造器,必须放在第一行,不会向上检索调用父类的构造器,必须放在第一行,不会向上检索

# 3、方法重写

子类可以继承父类的方法,但是我们不总是希望我们的方法和父类一模一样,总会有些变化,龙生九子,每个人都有每个人的特征。

那我们就要对我们继承下来的方法进行改造了,怎么改造?重写一下就可以了。

public class Father {
    public void eat(){
        System.out.println("我爱吃鱼!");
    }
}


public class SonONe extends Father {

    public void eat(){
        System.out.println("我爱吃虾!");
    }

    public static void main(String[] args) {
        SonONe sonONe = new SonONe();
        sonONe.eat();
    }
}

结果:
    我爱吃虾!

重写一定要保证参数、名字全部一样。咱们还学过一个重载还记得吗?

返回值要一样,或者返回父类的子类型。不好理解,学了多态,回头看。

名称范围方法名形参列表返回类型权限修饰
重载(overload)本类必须一样类型,个数或者顺序不同,名字无所谓没有要求无要求
重写(override)父子类必须一样必须相同一样,或者子类的返回值是父类的返回值的子类子类不能缩小父类的访问权限

重写中子类要求更小的返回值范围和更大的权限范围,这两个问题需要结合多态来聊,咱们暂且放下,等学完多态了回头思考。

# 4、final关键字

目前为止,我们的继承学的差不多了。回想我们之前遗留的一些知识

1、权限修饰符的这两条应该不用解释了吧!

image-20210816170945914image-20210816170945914

2、String这个对象被final修饰,final究竟有什么作用。

public final class String
    implements java.io.Serializable, Comparable<String>,CharSequence
private final char value[];


JDK9以后
@Stable
private final byte[] value;
使用byte数组可以减少一半的内存,byte使用一个字节来存储一个char字符,char使用两个字节来存储一个char字符。只有当一个char字符大小超过0xFF时,才会将byte数组变为原来的两倍,用两个字节存储一个char字符。

final作为一个关键字,他可以修饰变量,方法,以及类,final就是最终的意思:

1、被final修饰的变量不能被修改,这里有两层含义,如果final修饰的是基础数据类型是值不能被修改,如果是引用数据类型就是引用指向不能被修改。

image-20210816171317971image-20210816171317971

2、被final修饰的方法不能被重写

image-20210816171408518image-20210816171408518

3、被final修饰的类不能被继承

image-20210816171429912image-20210816171429912

# 5、祖先类Object

Object类有11个方法,其中有八个是公共方法:

方法描述
boolean equals(Object obj)指示某个其他对象是否与此对象“相等”
public native int hashCode();返回该对象的哈希码值
String toString()返回该对象的字符串表示
Class<? extendsObject> getClass()返回一个对象的运行时类
void notify()唤醒在此对象监视器上等待的单个线程
void notifyAll()唤醒在此对象监视器上等待的所有线程
void wait()导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法
void wait(long timeout)导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量
void wait(long timeout, int nanos)导致当前的线程等待,直到其他线程调用此对象的 notify()
protected native Object clone()克隆对象,浅拷贝
protected void finalize()垃圾回收器准备释放内存的时候,会先调用finalize()。

显示详细信息

其中notify和wait相关的代码都是和线程相关的,我们会在讲线程的时候一一讲解。

# 1、hashcode

这个方法是这么定义的,所有带有native的方法都是本地方法,他不是java写的。这个hashcode的返回值其实是实例对象运行时的内存地址。

public native int hashCode();

什么是hash算法:

一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。

hash算法有几个特点:

1、只能通过原文计算出hash值,而且每次计算都一样,不能通过hash值计算原文。

2、原文的微小变化就能是hash值发生巨大变化。

3、一个好的hash算法还能尽量避免发生hash值重复的情况,也叫hash碰撞。

hash的用途:

1、密码的保存:

实际的工程当中我们一般不存储明文密码,而是将密码使用hash算法计算成hash值进行保存。这样即使密码丢失也不会使密码完全曝光。

image-20210816151639948image-20210816151639948

2、文件的校验,检查数据的一致性

image-20210816152722879image-20210816152722879

# (1)常见的Hash摘要算法

请记住我们不是研究这些算法的人,而是使用这些算法的人。研究这些算法的事情交给科学家,我们使用其实是很简单的

MD5

介绍:MD5信息摘要算法(英语:MD5 Message-Digest Algorithm),一种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值(hash value),用于确保信息传输完整一致。MD5由美国密码学家【罗纳德·李维斯特】设计,于1992年公开,用以取代MD4算法。1996年后该算法被证实存在弱点,可以被加以破解,对于需要高度安全性的数据,专家一般建议改用其他算法,如SHA-2。

public static void main(String[] args) throws Exception {
    MessageDigest md5 = MessageDigest.getInstance("MD5");
    byte[] digest = md5.digest("123".getBytes());
    System.out.println(Arrays.toString(digest));
}
[32, 44, -71, 98, -84, 89, 7, 91, -106, 75, 7, 21, 45, 35, 75, 112]

SHA1

SHA-1(英语:Secure Hash Algorithm 1,中文名:安全散列算法1)是一种密码散列函数,美国国家安全局设计,并由美国国家标准技术研究所(NIST)发布为联邦数据处理标准(FIPS)。SHA-1可以生成一个被称为消息摘要的160位(20字节)散列值,散列值通常的呈现形式为40个十六进制数。

SHA 家族

正式名称为 SHA 的家族第一个成员发布于 1993年。然而人们给它取了一个非正式的名称 SHA-0 以避免与它的后继者混淆。两年之后, SHA-1,第一个 SHA 的后继者发布了。 另外还有四种变体,曾经发布以提升输出的范围和变更一些细微设计: SHA-224, SHA-256, SHA-384 和 SHA-512 (这些有时候也被称做 SHA-2):

public static void main(String[] args) Exception {
        MessageDigest sha1 = MessageDigest.getInstance("SHA1");
        byte[] digest = sha1.digest("123".getBytes());
        System.out.println(Arrays.toString(digest));
}

[64, -67, 0, 21, 99, 8, 95, -61, 81, 101, 50, -98, -95, -1, 92, 94, -53, -37, -66, -17]

SHA256

SHA256算法使用的哈希值长度是256位。

public static void main(String[] args) throws Exception {
    MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
    byte[] digest = sha256.digest("123".getBytes());
    System.out.println(Arrays.toString(digest));
}

[-90, 101, -92, 89, 32, 66, 47, -99, 65, 126, 72, 103, -17, -36, 79, -72, -96, 74, 31, 63, -1, 31, -96, 126, -103, -114, -122, -9, -9, -94, 122, -29]

SHA512

算法使用的哈希值长度是512位。

public static void main(String[] args) throws Exception {
    MessageDigest sha512 = MessageDigest.getInstance("SHA-512");
    byte[] digest = sha512.digest("123".getBytes());
    System.out.println(digest.length);
}

[60, -103, 9, -81, -20, 37, 53, 77, 85, 29, -82, 33, 89, 11, -78, 110, 56, -43, 63, 33, 115, -72, -45, -36, 62, -18, 76, 4, 126, 122, -79, -63, -21, -117, -123, 16, 62, 59, -25, -70, 97, 59, 49, -69, 92, -100, 54, 33, 77, -55, -15, 74, 66, -3, 122, 47, -37, -124, -123, 107, -54, 92, 68, -62]

# 2、equals

其实这个方法我们已经讲过了,我们当时比较两个字符串的时候就使用了这个方法,但是我们不妨看看在Object中equals的实现是什么样子。

public boolean equals(Object obj) {
    return (this == obj);
}

从源码中我们看到默认的时间就是==

小提示:以后我们比较所有的引用数据类型的时候,都要使用equals。

还记得我们当时的例子吗?

public static void main(String[] args) {
    Integer num2 = 128;
    Integer num1 = 128;

    System.out.println(num1 == num2);
}
false

你在问你,比较两个Integer用==还是equals:

重要总结】:==和equals的区别

1、==可以比基础数据类型也可以比较引用数据类型,比较基础数据类型时比较的是具体的值,比较引用数据类型实际上比较的是内存地址。

2、equals是Object的一个方法,默认的实现就是 ==。

3、我们可以重写equals方法,是我们的特性需求,比如String就重写了equals方法,所以字符串调用equals比较的是每一个字符。

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

jdk11

public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String aString = (String)anObject;
            if (coder() == aString.coder()) {
                return isLatin1() ? StringLatin1.equals(value, aString.value)
                                  : StringUTF16.equals(value, aString.value);
            }
        }
        return false;
    }

我们点击进入StringUTF16.equals方法。

@HotSpotIntrinsicCandidate
    public static boolean equals(byte[] value, byte[] other) {
        if (value.length == other.length) {
            int len = value.length >> 1;
            for (int i = 0; i < len; i++) {
                if (getChar(value, i) != getChar(other, i)) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }
}


public static boolean equals(byte[] value, byte[] other) {
        if (value.length == other.length) {
            for (int i = 0; i < value.length; i++) {
                if (value[i] != other[i]) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }
}

【作业】编写一个类Student,我们可以比较两个学生,如果编号和名字一样,我就认为是同一个人。

public class Student {
    
    private int id;
    private String name;
    private int age;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null) return false;
        Student student = (Student) o;
        return id == student.id && student.getName().equals(name);
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
public static void main(String[] args) throws NoSuchAlgorithmException {
    Student student = new Student(1,"zhangsan",12);
    Student student2 = new Student(1,"zhangsan",13);
    System.out.println(student.equals(student2));
}

結果:
    true

# 3、toString()

还记得我们的arrayToString()的方法吗?这个方法就是把一个实例对象转化成一个可打印的字符串。

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
System.out.println(dog)

我们默认的打印的方法就是默认调用dog的toString方法。

【作业】编写一个Student类,打印出以下内容:

public static void main(String[] args) throws NoSuchAlgorithmException {
    Student student = new Student(1,"zhangsan",12);
    System.out.println(student);
}

结果:

Student{id=1, name='zhangsan', age=12}

# 4、finalize()

java提供finalize()方法,垃圾回收器准备释放内存的时候,会先调用finalize()。其实我们无法保证fnalize什么时候执行,执行的是否符合预期。使用不当会影响性能,导致程序死锁、挂起等。 垃圾回收和finalize()都是靠不住的,只要JVM还没有快到耗尽内存的地步,它是不会浪费时间进行垃圾回收的。

对于fnalize,我们要明确它是不推荐使用的,业界实践一再证明它不是个好的办法,在Java 9中,甚至明确将Object.fnalize()标记为过时!如果没有特别的原因,不要实现fnalize方法,也不要指望利用它来进行资源回收。

# 5、clone()

克隆就是在内存里边赋值一个实例对象。但是Object的克隆方法只能浅拷贝。同时必须实现Cloneable接口。深拷贝我们以后会讲解。

public class Dog implements Cloneable {

    public Dog newDog(){
        try {
            return (Dog)this.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }

    public static void main(String[] args) {
        Dog dog = new Dog();
        Dog newDog = dog.newDog();
        System.out.println(dog == newDog);
    }
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

# 四、面向对象之多态(polymorphism)

# 1、概述

这一章节是javase当中最难的一块知识,在学习多态之前,我们先问你几个问题:

  • 狗是动物吗? 是
  • 猫是动物吗? 是
  • 狗是猫吗? 否

这种问题看似及其简单,这和写代码有什么关系呢?但我们要明白,编程源于生活,高于生活。这是生活的常理,也可以是编程的常理。

我们不妨试想,这样的代码对不对:

// 定义了一个动物,他是狗
Animal dog = new Dog();
// 定义了一个动物,他是猫
Animal cat = new Cat();

你当然不能这样写:Dog cat = new Cat();

我们接着再来提问?

小丽同学想养一个宠物:

  • 狗可以吗?
  • 猫可以吗?
  • 好像都可以

那我们能不能写这样的代码:

有一个方法:
// 姑娘想养一只动物
public class Girl{
	public void feed(Animal animal){}
}

调用
girl.feed(new Dog());
girl.feed(new Cat());

有一个方法:
// 姑娘想养一条狗
public class Girl{
	public void feed(Dog dog){}
}


调用
girl.feed(new Dog());
girl.feed(new Cat());

在我们生活中这样表述理所当然,但是代码中呢?其实大致也是可以的。其实这就是多态一种宏观的通俗的理解,我觉得可以简单的这么说,动物可以有多种实现的形态,但这绝对不是多态的正确理解,接下来我们从编程的角度去思考这个事情。

我们现在修改Girl的feed的方法:

public void feed(Animal animal){
	animal.coquetry();
}

同时给animl和他的子类增加【撒娇】的方法

public class Animal{
	public void coquetry(){
		System.out.println("动物在撒娇");
	}
}

public class Dog extends Animal{
	public void coquetry(){
		System.out.println("狗在撒娇");
	}
}

那么,一下场景会输出什么结果:

Animal dog = new Dog();
girl.feed(dog);

有些人会理所当然的认为输出结果是【狗在撒娇】,但是却不知道其中的原理,当然这个结果是对的。

但是我如果说,我们定义分明就是一个Animal呀!你可能就不知道如何解释了。

我们在很多网站上可以看到对于多态的形成条件有以下三个条件:

1、有继承

2、有重写

3、有父类引用指向子类对象

这种说法在宏观上是正确的,接下来我们就去探究一下具体的调用逻辑,或许你会大吃一惊。

一下内容比较底层,看懂了你会对一个对象的方法调用了解的很深刻,看不懂也没有太大的关系。

# 2、多态的底层原理

# (1)字节码分析

一段程序从我们写代码到运行阶段会经历编译和运行两个阶段,编译是将 .java文件转化成为 jvm识别的字节码文件,jvm会将字节码文件加载到内存,并执行。

我们现在给分析一下下面这段代码:

Animal animal = Math.random() > 0.5 ? new Dog():new Cat(); 

请问,对于引用 animal 来说,它到底是Animal类型还是Dog类型,还是Cat类型,在程序为运行的时候这个Animal就如同一只【薛定谔的猫】,有50%的可能是猫,也有50%的可能是狗。

其实我们可以发现,对于这一段代码来讲:Animal animal =new Dog() 等于号左右貌似是确定的,永远不能变,而右侧则不一样了,在代码未执行,其实我们也无法知道。

那我们就叫 Animal为 animal 的【静态类型】,或者叫【编译类型】或者叫【申明类型】,或者叫【外观类型】。而等于右侧的我们叫【动态类型】,也叫【运行时类型】或者叫【实际类型】。

对于静态类型,jvm在编译的时候就能确定具体调用哪个版本的方法,字节码指令执行时直接调用即可,而动态类型必须等待运行时才能确定类型,与此同时才能同步开展选择方法版本的工作,这个运行时才选择方法调用版本的行为称之为【虚方法分派】。

我们之前给大家讲过【常量池】,常量池是我们的资源仓库,里边保存了大量的符号引用(就是的你给类、方法、变量的名字),这些符号引用有一部分会在类加载阶段或者第一次使用的时候就被转化为【直接引用】,这种转化叫做【静态解析】,另一部分会在运行期间转化为【直接引用】,这一部分称之为【动态链接】。

举一个例子:

乔峰要找段誉喝酒,段誉就是符号引用,乔峰开始找段誉就是运行时,这是段誉在醉仙楼,那么我们就可以将乔峰要找段誉喝酒,转化为乔峰去醉仙楼喝酒,这个过程是静态解析。

乔峰要找带头大哥报仇,这是乔峰得知自己身世后,如果不去寻找真凶,仇人就是带头大哥,在报仇期间,带头大哥一会是段正淳、一会是方丈、一会是慕容博,不到最后真正报仇的时候,他根本不知道仇人的具体位置。这个过程是动态链接。

这两个小案例的区别在于,第一个乔峰找的就是段誉,第二个乔峰找的是仇人。

我知道你听不懂,不要紧,我们在字节码文件入手,如果觉得有困难可以忽略本章节,用最深刻的方式去理解即可。

写一个简单的小例子,咱们慢慢分析这个引用的转化过程:

public class Computer {

    public int plus(int i,int j){
        return i+j;
    }

    public static void main(String[] args) {
        Computer computer = new Computer();
        System.out.println(computer.plus(2,4));
    }

}

编译之后使用notepad++ 打开,这里要点击右侧的“H”,这个插件我教大家安装过,我们可以看到他的字节码文件。

序号jdk版本major.minor version
7751
8852

image-20210818114754607image-20210818114754607

直接阅读字节码太难了,谁也受不了。还好,java给我提供了一个很好的工具,叫javap能够将上面的字节码文件转化成我们大致能看懂的形式。

在out目录的class文件处右键选择Open in —> Terminal,在命令终端打开,这就和咱们的cmd一样样的,你用cmd也行,和javac的使用方法一致。

 javap -verbose  .\Animal.class

这样能够比较清晰的展示我们的字节码文件的内容:

Classfile /D:/code/test/out/production/test/com/ydlclass/Computer.class
  Last modified 2021-8-18; size 678 bytes
  MD5 checksum 66c4346d383bfe69633628dc2f921744
  Compiled from "Computer.java"
public class com.ydlclass.Computer
  minor version: 0
  major version: 55
  flags: ACC_PUBLIC, ACC_SUPER
  // 常量池就这样展现给了我们,看看你写的那些字符串和你写的类名、变量名是不是都在这里
Constant pool:
   #1 = Methodref          #7.#27         // java/lang/Object."<init>":()V
   #2 = Class              #28            // com/ydlclass/Computer
   #3 = Methodref          #2.#27         // com/ydlclass/Computer."<init>":()V
   #4 = Fieldref           #29.#30        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #2.#31         // com/ydlclass/Computer.plus:(II)I
   #6 = Methodref          #32.#33        // java/io/PrintStream.println:(I)V
   #7 = Class              #34            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               Lcom/ydlclass/Computer;
  #15 = Utf8               plus
  #16 = Utf8               (II)I
  #17 = Utf8               i
  #18 = Utf8               I
  #19 = Utf8               j
  #20 = Utf8               main
  #21 = Utf8               ([Ljava/lang/String;)V
  #22 = Utf8               args
  #23 = Utf8               [Ljava/lang/String;
  #24 = Utf8               computer
  #25 = Utf8               SourceFile
  #26 = Utf8               Computer.java
  #27 = NameAndType        #8:#9          // "<init>":()V
  #28 = Utf8               com/ydlclass/Computer
  #29 = Class              #35            // java/lang/System
  #30 = NameAndType        #36:#37        // out:Ljava/io/PrintStream;
  #31 = NameAndType        #15:#16        // plus:(II)I
  #32 = Class              #38            // java/io/PrintStream
  #33 = NameAndType        #39:#40        // println:(I)V
  #34 = Utf8               java/lang/Object
  #35 = Utf8               java/lang/System
  #36 = Utf8               out
  #37 = Utf8               Ljava/io/PrintStream;
  #38 = Utf8               java/io/PrintStream
  #39 = Utf8               println
  #40 = Utf8               (I)V
{
  public com.ydlclass.Computer();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         // 构造器编译之后也是符号引用,就是名字,加载到内存之后,我们就可以解析成对应的方法的调用地址了,			
         // 仅仅知道名字是无法调用或执行方法的,必须知道方法在内存的具体位置
         // 在一个类加载到内存之后,将名字转化为地址的过程叫解析
         // 构造器的调用没有什么特殊的确定是哪个就是哪个,所有类加载的时候就能完成解析过程
         // 这就是静态解析
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/ydlclass/Computer;

  public int plus(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: ireturn
      LineNumberTable:
        line 6: 0
      // 本地变量表,很明显这里有三个,this 、 i、 j,我们可以画图解析一下这个过程,如下图
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  this   Lcom/ydlclass/Computer;
            0       4     1     i   I
            0       4     2     j   I

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=2, args_size=1
         0: new           #2                  // class com/ydlclass/Computer
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        11: aload_1
        12: iconst_2
        13: iconst_4
        // 调用过程和构造器有类似的地方,但是调用一个类的方法,会出现很多可能
        // 1、本类没有这个方法,父类有,或者爷爷有
        // 2、调用这个方法的实例对象在编译器是否能确定,未必的,这个和构造器有很大区别
        // 所以这类方法(虚方法)调用在编译期间不能确定调用的版本的
        // 这也就意味着加载后不能解析完成,需要在运行时根据实际情况在进行解析,这叫动态解析
        14: invokevirtual #5                  // Method plus:(II)I
        17: invokevirtual #6                  // Method java/io/PrintStream.println:(I)V
        20: return
      LineNumberTable:
        line 10: 0
        line 11: 8
        line 12: 20
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      21     0  args   [Ljava/lang/String;
            8      13     1 computer   Lcom/ydlclass/Computer;
}
SourceFile: "Computer.java"

# (2)方法在栈内的调用

来一段小插曲,方法在栈内的调用过程是什么样呢?为什么方法调用要选择栈这种数据结构呢?

现在我们已经知道了栈这种数据结构是先进后出的:

现在有这么一段代码:

public void a(){}

public void b(){ a();}

public void c(){ b();}

public static void main(String[] args) {
    Computer computer = new Computer();
    computer.c();
}

从方法的级别来思考,大概是这么一个过程,其实我们总能发现一个规律:

最先执行的方法最后结束,最后执行的方法最先结束,是不是很像栈的特点

image-20210818132422744image-20210818132422744

我们单独拿出plus方法的字节码,每个方法栈帧内还有【操作数栈】,也是一种先进后出的数据结构,用来执行本方法的指令,操作数栈在执行前就能确定具体的深度:

public int plus(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC
    Code:
	  // 这里告诉你操作数栈深度为2,本地变量有3个,参数有三个
      stack=2, locals=3, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: ireturn
      LineNumberTable:
        line 6: 0
      // 本地变量表,很明显这里有三个,this 、 i、 j,我们可以画图解析一下这个过程,如下图
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  this   Lcom/ydlclass/Computer;
            0       4     1     i   I
            0       4     2     j   I

flags:访问权限

code区域:

stack 操作数栈的深度

locals 局部变量的个数

args_size 参数的个数

LocalVariableTable本地变量表:这里有三个变量,三个solt,

 0: iload_1
 1: iload_2
 2: iadd
 3: ireturn

以上这些区域组成了我们的方法表,方法是用来描述方法的。

前边的数字是:程序计数器,请记住这个概念,一会读的时候回再次提及,用来计算下一次指令的偏移量。

文章底部有我们的所有的jvm指令速查表。

iload_1 将第二个 int 型本地变量推送至栈顶

iload_2 将第三个 int 型本地变量推送至栈顶

image-20210818133947930image-20210818133947930

iadd 做加法,这个指令是从栈顶取出两个元素(两次出栈),相加后在压入栈顶

image-20210818134112460image-20210818134112460

希望这个过程的简单描述,能让大家对栈、栈帧有一定的了解。

我们了解了一个方法在jvm内的布局之后,再去看看一个方法被调用的过程。

我们紧接着看看这个main方法:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=2, args_size=1
      	 // 创建一个对象,并将其引用值压入栈顶
         0: new           #2                  // class com/ydlclass/Computer
         // 复制栈顶数值并将复制值压入栈顶
         3: dup
         // 很明显这是在调用构造器,编译之后还是符号引用,就是方法的字符串形式的名字,
         // 加载之后,我们就可以解析成对应的方法的调用地址了
         // 因为一旦类加载到内存的方法区,这个方法就有了真实的调用地址了
         4: invokespecial #3                  // Method "<init>":()V
         // 将栈顶引用型数值存入第二个本地变量
         7: astore_1
         // 获取指定类的静态域,并将其值压入栈顶
         8: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
         // 将第二个引用类型本地变量推送至栈顶
        11: aload_1
        // 将 int 型 2 推送至栈顶
        12: iconst_2
        // 将 int 型 4 推送至栈顶
        13: iconst_4
        // 调用实例方法,调用的过程是在内存进行的,只有当字节码被加载进入内存才有具体的地址
        14: invokevirtual #5                  // Method plus:(II)I
                 
                  // 以下部分是粘贴过来的plus方法的,此时会创建新的栈帧
                  // 单独这个方法的指令入口在编译的时候是不可知的,但是加载到内存就可知了
                  // 其实,这个调用的不一定是这个方法,只是为了演示
                 -------------------------
                 // 将第二个 int 型本地变量推送至栈顶
                 0: iload_1
                 // 将第三个 int 型本地变量推送至栈顶
                 1: iload_2
                 // 将栈顶两 int 型数值相加并将结果压入栈顶
                 2: iadd
                 3: ireturn 
             	 -------------------------   
        17: invokevirtual #6                  // Method java/io/PrintStream.println:(I)V
        20: return
      LineNumberTable:
        line 10: 0
        line 11: 8
        line 12: 20
      LocalVariableTable:
        // 这里的Signature就是一个引用的静态类型,这里早有记录
        Start  Length  Slot  Name   Signature
            0      21     0  args   [Ljava/lang/String;
            8      13     1 computer   Lcom/ydlclass/Computer;
}

我们在代码中看到invokespecial,和invokevirtual这样的指令用来调用方法,当然还有invokestatic。这些指令是有区别的。

invokespecial用来调用构造方法,invokestatic用来调用静态方法,invokeinterface用来调用接口方法,invokespecial用来调用实例方法(虚方法)。这里有些没学呢,先不用管它。

被invokestatic、invokeinterface 和 invokespecial指令调用的方法,一定能在解析阶段(加载完成后或第一次使用)确定唯一的调用版本,比如静态方法,私有方法,和实例构造器、被final修饰的方法。调用会在类加载的时候就能顺序解析成直接引用,这类方法叫非虚方法,反之都是虚方法,这里边有个特例,就是final修饰的方法也是被invokevirtual 调用,这是历史原因。

invokevirtual指令在执行的时候他会这样执行:

1、找到栈顶的元素所指向的实际类型,Dog(这个指令一旦执行,前边必然会有一个指令将实际类型压入栈顶)

2、在Dog中找调用的方法,如果找到了,直接调用

3、如果找不到,讲由下自上沿着继承关系,从父类中找

不妨再看:

// 将第二个引用类型本地变量推送至栈顶
11: aload_1
// 将 int 型 2 推送至栈顶
12: iconst_2
// 将 int 型 4 推送至栈顶
13: iconst_4
//  方法的两个参数会从栈顶依次获取,而方法调用时会到栈顶的元素所指向的实际类型
//  此时的栈顶已经是aload_1指令压入的变量了,二这个变量的实际类型是Computer(此处传递的是运行是类型)
14: invokevirtual #5                  // Method plus:(II)I

解析调用,是静态过程,在编译期间就能完全确定一个调用的方法版本,不必推迟到运行期间

在这个字节码之旅中,我们要搞懂几个概念:

【虚方法】、【编译】、【类加载 】(后边有章节会深入了解类加载) ,【静态解析】,【动态链接】以及【动态类型】和【静态类型】。如果真的掌握了,那么我们就可以接着学习了。

# 3、重载方法的调用

我们在调用一个虚方法的时候,jvm会在适当的时候帮我们选择合适的方法版本,有的时候在编译期、有时是在运行时,这个方法版本的选择过程我们可以称之为【方法分派】。

首先咱们看一个例子:

public class Human {
}

public class Man extends Human{
}

public class Woman extends Human{
}
public class Party {
    
    public void play(Human human){
        System.out.println("人类的狂欢!");
    }

    public void play(Man man){
        System.out.println("男人的狂欢!");
    }

    public void play(Woman woman){
        System.out.println("女人的狂欢!");
    }
    
    public static void main(String[] args) {
        Party party = new Party();
        Human human = new Human();
        party.play(human);
        Human man = new Man();
        party.play(man);
        Human woman = new Woman();
        party.play(woman);
    }
}

结果:

人类的狂欢!
人类的狂欢!
人类的狂欢!

我们赫然发现最后的结果是三个人类的狂欢,这个结果可能让一些工作两三年的程序员大跌眼镜。

有了之前的铺垫,我们就能很好的解释这个问题了。

虚拟机在选择重载方式时,是通过【静态类型】决定的而不是动态类型。由于静态类型编译时就可知,事实上虚拟在编译期就已经知道选择哪一个重载方法,并且把这个方法的符号引用写在了invokevirtual的指令中。

所有依赖【静态类型】决定方法执行版本的的分派动作称之为静态分派,有些博客也会说这个过程是解析而不是分派,JVM帮助我们选择一个合适的方法的时候,也是尽最大努力,选择它认为最合适的版本。因为确实存在诸如自动拆装箱,对象转型等问题,大家可以看一个变态的重载题目:

public class Overload {

    public void sayHello(Object arg){
        System.out.println("hello object");
    }

    public void sayHello(int arg){
        System.out.println("hello int");
    }

    public void sayHello(long arg){
        System.out.println("hello long");
    }

    public void sayHello(Character arg){
        System.out.println("hello Character");
    }

    public void sayHello(char arg){
        System.out.println("hello char");
    }

    public void sayHello(char... arg){
        System.out.println("hello char...");
    }


    public static void main(String[] args) {
        new Overload().sayHello('a');
    }

}

结果当然是char,
如果我删掉 sayHello(char arg)方法呢
    我能将当前
    
    
    hello int
    hello long
    hello Character
    hello object
    hello char...

# 4、重写方法的调用

这个方法的调用过程其实我们已经分析的很清楚了。

invokevirtual指令在执行的时候他会这样执行:

1、找到栈顶的元素所指向的实际类型,Dog(这个指令一旦执行,前边必然会有一个指令将实际类型压入栈顶)

2、在Dog中找调用的方法,如果找到了,直接调用

3、如果找不到,讲由下自上沿着继承关系,从父类中找

重写方法的调用时依据运行时的类型决定的。

# 5、重载和重写

重载只是选择了调用方法的版本。

重写是具体明确了调用谁的方法。

举一个更变态的例子

public class Animal {
    public void eat(){
        System.out.println("animal is eating!");
    }
    public void eat(String food){
        System.out.println("animal is eating "+food);
    }
}
public class Dog extends Animal{
    @Override
    public void eat() {
        System.out.println("dog is eating!");
    }

    @Override
    public void eat(String food) {
        System.out.println("dog is eating " + food);
    }

    public static void main(String[] args) {
        Animal animal = new Dog();
        animal.eat("meat");
    }

}

这个案例里边有重载,也有重写,最终会选择Dog类的(String food)方法,

第一步是静态分派的过程,jvm从Animal类的多个重载方法中选择了 Animal::eat(String food) 这个方法,并且生成指令 invokevirtual Animal::eat(String food)

第二步是动态分派的过程,是根据运行时类型确定具体调用谁的 eat(String food) 方法,因为运行时类型是Dog,所以最终的方法选择是 Dog::eat(String food)

这两个过程是相辅相成,不是有你没我的关系。

重载(overloading)和重写(overriding)是java多态性的体现,但是由于重载是静态分派的原因,有些博客不认为重载也能体现多态性,这个就见仁见智了。

多态只和方法有关和属性无关

# 6、对象的转型

  • 向上转型:子类对象转为父类,向上转型不需要显示的转化。 Father Father = son;

    向上转型会丢失子类独有的特性

  • 向下转型:父类对象转为子类,向下转型需要强制转化。 Son son = (Son)Father;

    向下转型可能会出现错误,需要谨慎。

还是以女孩养宠物为案例:

public class Girl {

    public void feed(Animal animal){
        if(animal instanceof Dog){
            // 向下转型,能获取dog独有的方法
            Dog dog = (Dog)animal;
        }
        if (animal instanceof Cat){
            Cat dog = (Cat)animal;
        }
        
    }
    public static void main(String[] args) {
        Girl girl = new Girl();
        // 向上转型,自动回丢失Dog的特性
        girl.feed(new Dog());
    }
}

在这个案例中,我么既存在向上转型,也存在向下转型。

提问为什么向上转型会丢失Dog的特性呢?

1、静态分派是根据静态类型选择对应的版本,向上转型了后静态分派的过程只能在Animal这个类型查找方法,所以dog的特有方法就丢失了。

2、动态分派的过程还是依靠运行时类型决定的所以调用的方法还是Dog类的。

也由此得出一个结论,一个对象能调用的方法由它的【静态类型】决定。

ava.lang.ClassCastException: com.ydlclass.Cat cannot be cast to com.ydlclass.Dog

# 7、抽象类和接口

面向对象程序设计(OOP)目前已经接近尾声,这个小结我们再介绍两个重要的概念。

java中除了类,还有抽象类和接口这两个概念,这其中有很多值得我们学习的地方,在理解和思考之前我们先用一个小结给大家看看java中怎么定义抽象类和接口。

# (1)抽象方法和抽象类的定义

一般的方法:

public class Animal {
    public void eat(){
        System.out.println("Animal is eating.");
    }
}

抽象方法:

public abstract class Animal {
    abstract void eat();
}

abstract void eat(); 去掉方法体,加一个abstract关键字就是一个抽象方法,如果一个类里有抽象方法,在类的申明上必须也要加上abstract,变成一个抽象类。我们要注意的是,抽象方法没有方法体,所以不能直接调用,也正是因为抽象方法没有方法体,所以我们不能直接构造一个抽象类。

其实值得我们思考的问题是,一个方法连方法体也没有,这究竟有什么用。答案是【约定】。

我们不能【直接构造抽象类】,但是子类可以继承抽象类,并且必须重写抽象方法,除非子类也是抽象类。这样就会对所有子类有了共同约束,同时父类已经实现的方法也能被所有的子类所复用。

顾名思义:

public abstract class Animal {
    abstract void eat();
}

这个抽象方法是为了约束子类的,让子类必须实现这个方法。

抽象类中除了拥有抽象方法,也可以拥有普通方法。

public abstract class Animal {
    abstract void eat();
    
    public void print(){
        System.out.println("I'm an Animal!");
    }
    
}

抽象类无法直接进行实例化操作,当一个类实例化之后,就意味着这个对象可以调用类中的属性或者方法了,但在抽象类里存在抽象方法,而抽象方法没有方法体,没有方法体就无法进行调用。既然无法进行方法调用的话,又怎么去产生实例化对象呢。

抽象类里中也可以和其他类一样拥有自己的成员变量:

public abstract class Animal {
	private String name;
}
  • 既然有成员变量,我们大致可以猜出抽象类是可以构造的,因为属性必须通过new去内存分配空间才能赋值啊。
  • 那么抽象类中一定存在构造方法,实例化的过程就是属性赋值的过程啊!

看一下下边的例子:

public abstract class Animal {
    
    // 但是我们不能直接new
    public Animal(){
        System.out.println("animal has created!");
    }
    
    abstract void eat();

    public void print(){
        System.out.println("I'm an Animal!");
    }

}
public class Cat extends Animal {
    public Cat(){
        System.out.println("cat has created!");
    }

    @Override
    void eat() {
        System.out.println("cat is eating!");
    }

    public static void main(String[] args) {
        new Cat();
    }
}

结果:
animal has created!
cat has created!

这个过程说明了,创建子类时,父类依然会被创建,抽象类只有在构建子类的时候才会被构建出实例。

【小问题】:抽象类可以用final声明么?

抽象类存在的目的就是为了让子类去继承,一个类被final修饰了,就失去了这个能力,结果当然是不行了。

总结一下

  1. 抽象方法必须为public或者protected(因为如果为private,则不能被子类继承,子类便无法实现该方法),缺省情况下默认为public;
  2. 抽象类不能直接实例化,需要依靠子类采用向上转型的方式处理;
  3. 抽象类必须有子类,使用extends继承,一个子类只能继承一个抽象类
  4. 子类(如果不是抽象类)则必须覆写抽象类之中的全部抽象方法(如果子类没有实现父类的抽象方法,则必须将子类也定义为为abstract类。);

# (2)接口的定义

其实接口是比抽象类更高级的抽象,当然抽象类也是比类更高级的抽象。接口中只能有方法的定义,而不能有实现:

public abstract class Animal {

    /**
     * 呼吸的方法
     */
    public abstract void breath();

    /**
     * 吃的方法
     */
    public abstract void eat();
}

我们可以更加优雅的表达出来:

public interface Animal {

    /**
     * 呼吸的方法
     */
    void breath();

    /**
     * 吃的方法
     */
    void eat();
}

abstrac 都不需要了,但是要使用关键字interface,这种类我们称之为【接口】。

接口中能定义抽象方法,不能有实例字段、不能有方法实现(静态的可以),java8以后在接口中可以定义默认方法,这个我们先放一放以后再讲。编写接口的目的在于对类的某些能力进行约定和规范,接口不能被实例化,没有构造器。

接口中的方法默认是public的,我们也推荐使用默认的,也就是我们定义接口时,不用写它的权限修饰符。但是因为接口是契约、是约定子类必须具备的某些能力,是需要子类去实现的,所以我们在写借口时,推荐使用javadoc的方式给接口加注释。

接口是多实现的,一个类可以实现多个接口,但是只能继承一个类。接口之间也可以相互继承

# (3)深入理解

我们学习了几天的面向对象

  • 继承是 is-a 的关系, dog is an animal。 man is a human。
  • 实现是 can-do的关系, 实现更体现一个类的能力,通过实现多个接口是可以聚合多个能力的。

举一个例子:

【鸟能飞】和【飞机能飞】。它们有功能的特质吗?其实也不太有,当时它们都能飞。

  • 我们在设计上就可以定一个接口,接口有fly的方法定义。
  • 接口是可以多实现的,所以鸟和飞机除了实现飞行的接口还能实现很多其他的接口。这也就意味着它们can-do 很多事情。

抽象类是模板式的设计,而接口是契约式设计。

抽象类设计时往往就是将相同实现方法抽象在父类,由子类独立实现那些实现各自不同的实现。

【做好顶层设计】

中央政府我我们规划蓝图,做好顶层设计,具体的实现具体来,只要跟着党的路线走就好了。

我们再举一个例子,比如食物链,动物会吃其他动物,也会被其他动物吃

public interface Animal {
    /**
     * 吃的方法
     */
    void eat(Animal animal);

    /**
     * 获取名字
     * @return
     */
    String getName();
}

老虎

public class Tiger implements Animal {

    @Override
    public void eat(Animal animal) {
        System.out.println(this.getName() + "吃了" + animal.getName());
    }

    @Override
    public String getName() {
        return "tiger";
    }
}

public class Wolf implements Animal {
    @Override
    public void eat(Animal animal) {
        System.out.println(this.getName() + "吃了" + animal.getName());
    }

    @Override
    public String getName() {
        return "wolf";
    }
}

public class Sheep implements Animal {
    @Override
    public void eat(Animal animal) {
        System.out.println(this.getName() + "吃了" + animal.getName());
    }

    @Override
    public String getName() {
        return "sheep";
    }
    
    public static void main(String[] args) {
        Animal tiger = new Tiger();
        Animal wolf = new Wolf();
        Animal sheep = new Sheep();
        wolf.eat(sheep);
        tiger.eat(wolf);
    }
}
结果:
wolf吃了sheep
tiger吃了wolf
        

公司里边,牛逼的人写接口。接口更多的是设计的工作,实现更多是搬砖的工作。

# 8、设计模式

设计模式是人们为软件开发中相同表征的问题,抽象出的可重复利用的解决方案。在某种程度上,设计模式已经代表了一些特定情况的最佳实践,同时也起到了软件工程师之间沟通的“行话”的作用。理解和掌握典型的设计模式,有利于我们提高沟通、设计的效率和质量 。

# 1、面向对象设计原则

不要求理解,说实话我工作多年也是知道

# (1)开闭原则(Open Close Principle)

开闭原则就是说对扩展开放,对修改关闭

可以通过“抽象约束、封装变化”来实现开闭原则,即通过接口或者抽象类为软件实体定义一个相对稳定的抽象层,而将相同的可变因素封装在相同的具体实现类中。而软件中易变的细节可以从抽象派生来的实现类来进行扩展,当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了。

# (2)里氏代换原则(Liskov Substitution Principle)

继承必须确保超类所拥有的性质在子类中仍然成立。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。

# (3)依赖倒转原则(Dependence Inversion Principle)

要面向接口编程,不要面向实现编程。

  1. 每个类尽量提供接口或抽象类,或者两者都具备。
  2. 变量的声明类型尽量是接口或者是抽象类。
  3. 任何类都不应该从具体类派生。
  4. 使用继承时尽量遵循里氏替换原则。
# (4)接口隔离原则(Interface Segregation Principle)

要求程序员尽量将臃肿庞大的接口拆分成更小的和更具体的接口,让接口中只包含客户感兴趣的方法。

这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。还是一个降低类之间的耦合度的意思,从这儿我们看出,其实设计模式就是一个软件的设计思想,从大型软件架构出发,为了升级和维护方便。所以上文中多次出现:降低依赖,降低耦合。

# (5)迪米特法则(最少知道原则)(Demeter Principle)

只与你的直接朋友交谈,不跟“陌生人”说话(Talk only to your immediate friends and not to strangers)。其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。

# (6)合成复用原则(Composite Reuse Principle)

原则是尽量使用合成/聚合的方式,而不是使用继承。如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。

# (7)单一原则

一个类只做一件事情

# (2)模板方法设计模式

模板方法(Template Method)模式的定义如下:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。它是一种类【行为型模式】。

该模式的主要优点如下:

  1. 它封装了不变部分,扩展可变部分。它把认为是不变部分的算法封装到父类中实现,而把可变部分算法由子类继承实现,便于子类继续扩展。
  2. 它在父类中提取了公共的部分代码,便于【代码复用】。
  3. 部分方法是由子类实现的,因此子类可以通过扩展方式增加相应的功能,符合开闭原则。

该模式的主要缺点如下。

  1. 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象,间接地增加了系统实现的复杂度。
  2. 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。
  3. 由于继承关系自身的缺点,如果父类添加新的抽象方法,则所有子类都要改一遍。

咱们写一个例子:

一个囚犯(prisoner),的一天,起床 吃饭 劳动 吃饭 劳动 看新闻 吃饭 睡觉

对于一个囚犯来说每天都是这样来的。

public abstract class Prisoner {

    protected String name;

    /**
     * 劳动的方法
     */
    abstract void work();

    /**
     * 吃的方法
     */
    abstract void eat();

    /**
     * 看新闻
     */
    abstract void readNews();

    /**
     * 一天的生活
     */
    public void life(){
        eat();
        work();
        eat();
        work();
        eat();
        readNews();
    }
}
public class Star extends Prisoner{

    public Star(String name) {
        this.name = name;
    }

    @Override
    void work() {
        System.out.println(this.name + "一直很烦恼的干活!");
    }

    @Override
    void eat() {
        System.out.println(this.name + "吃不下牢里的饭");
    }

    @Override
    void readNews() {
        System.out.println(this.name + "一边看新闻,一边想选妃的辉煌时刻。");
    }

}
public class Visitor {

    public void visit(Prisoner prisoner){
        System.out.println("我开始参观体会囚犯的一天");
        prisoner.life();
        System.out.println("一天的参观结束");
    }

    public static void main(String[] args) {
        Prisoner wxf = new Star("吴亦凡");
        Visitor jerry = new Visitor();
        jerry.visit(wxf);
    }
}


我开始参观体会囚犯的一天
吴亦凡吃不下牢里的饭
吴亦凡一直很烦恼的干活!
吴亦凡吃不下牢里的饭
吴亦凡一直很烦恼的干活!
吴亦凡吃不下牢里的饭
吴亦凡一边看新闻,一边想选妃的辉煌时刻。
一天的参观结束

这样设计有什么好处:

每一个子类都不需要关心每天的生活流程,因为法律已经规定了。

每一类人根据自己的特性、状态需求完成自己的实现就好了。

# (2)策略设计模式

策略设计模式有难度,可以不学

聊一聊对象数组排序,要排序就要有个两两比较的过程。

我们怎么比较两个对象,取出每一个值进行比较也行,但是写出的代码不优雅,还记得我们学过的equals方法吗?

我们既然能做等值比较,为什么不能做大小的比较呢?

public class User {

    private String username;
    private Integer age;
    private Double height;

    public User(String username, Integer age, Double height) {
        this.username = username;
        this.age = age;
        this.height = height;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Double getHeight() {
        return height;
    }

    public void setHeight(Double height) {
        this.height = height;
    }

    @Override
    public String toString() {
        return "User{" +
                "username='" + username + '\'' +
                ", age=" + age +
                ", height=" + height +
                '}';
    }

    public static void main(String[] args) {
        User user1 = new User("小王",12,153.4);
        User user2 = new User("小李",14,163.4);
        User user3 = new User("小张",13,123.4);
        User user4 = new User("小杨",4,6.4);
        
        User[] users = {user1,user2,user3,user4};
        for (int i = 0; i < users.length-1; i++) {
            for (int j = 0; j < users.length - i - 1; j++) {
                if(users[j].age > users[j+1].age){
                    User temp = users[j];
                    users[j] = users[j+1];
                    users[j+1] = temp;
                }
            }
        }
        for (int i = 0; i < users.length; i++) {
            System.out.println(users[i]);
        }
    }
}


结果没问题:
User{username='小杨', age=4, height=6.4}
User{username='小王', age=12, height=153.4}
User{username='小张', age=13, height=123.4}
User{username='小李', age=14, height=163.4}

这样有什么问题啊,

public class User implements Comparable {

    private String username;
    private Integer age;
    private Double height;

    public User(String username, Integer age, Double height) {
        this.username = username;
        this.age = age;
        this.height = height;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Double getHeight() {
        return height;
    }

    public void setHeight(Double height) {
        this.height = height;
    }

    @Override
    public String toString() {
        return "User{" +
                "username='" + username + '\'' +
                ", age=" + age +
                ", height=" + height +
                '}';
    }

    public static void main(String[] args) {
        User user1 = new User("小王",12,153.4);
        User user2 = new User("小李",14,163.4);
        User user3 = new User("小张",13,123.4);
        User user4 = new User("小杨",4,6.4);

        User[] users = {user1,user2,user3,user4};
        for (int i = 0; i < users.length-1; i++) {
            for (int j = 0; j < users.length - i - 1; j++) {
                if(users[j].compare(users[j+1]) > 0){
                    User temp = users[j];
                    users[j] = users[j+1];
                    users[j+1] = temp;
                }
            }
        }
        for (int i = 0; i < users.length; i++) {
            System.out.println(users[i]);
        }
    }

    @Override
    public int compare(Object object) {
        if(object instanceof User){
            User user = (User)object;
            if(this == user){
                return 0;
            } else {
                if(this.getAge() > user.getAge()){
                    return 1;
                } else if (this.getAge() < user.getAge()){
                    return -1;
                } else {
                    return 0;
                }
            }
        }
        return -1;
    }
}

对于上边的编写代码的方式,我们看看有没有什么值得优化的地方?

如果我们想修改比较的内容,就必须修改User类,这很明显违反了开闭原则。

1、User不变

2、写一个比较器的接口

public interface Comparator {
    int compare(User user1,User user2);
}

3、写一个比较器

public class UserAgeComparator implements Comparator {
    @Override
    public int compare(User user1, User user2) {
        return user1.getAge() - user2.getAge();
    }
}

4、写一个工具类专门给User排序

public class SortUtil {

    public void sort(User[] users, Comparator comparator){

        for (int i = 0; i < users.length-1; i++) {
            for (int j = 0; j < users.length - i - 1; j++) {
                if(comparator.compare(users[j],users[j+1]) > 0){
                    User temp = users[j];
                    users[j] = users[j+1];
                    users[j+1] = temp;
                }
            }
        }
    }
}

5、排序开始

public static void main(String[] args) {
        User user1 = new User("小王",12,153.4);
        User user2 = new User("小李",14,163.4);
        User user3 = new User("小张",13,123.4);
        User user4 = new User("小杨",4,6.4);

        User[] users = {user1,user2,user3,user4};

        new SortUtil().sort(users,new UserAgeComparator());

        for (int i = 0; i < users.length; i++) {
            System.out.println(users[i]);
        }
    }

它好在哪里了呢?

如果将来我们需要按照身高或者其他的方式排序呢?

我们再写一个排序的比较器就行了:

public class UserAgeComparator implements Comparator {
    @Override
    public int compare(User user1, User user2) {
        return user1.getAge() - user2.getAge();
    }
}

同时User也不需要直接实现某些接口,是不是很棒。

其实我们还能结合我们超级数组来使用,大家不妨试一试。

# 五、面向对象的其他知识

# 1、代码块

代码块又称初始化块,属于类中的成员,它是讲逻辑语句封装在方法体中,通过{} 包裹。代码块没有方法名,没有参数,没有返回值,只有方法体,而且不通过对象或类进行显示的调用,他会在类加载,或者创建对象时主动的隐式调用。

# (1)静态代码块

一个类被加载时会被调用一次,常用在需要做一些全局初始化的工作。

static {

}

# (2)实例代码块

每次创建实例,都会被调用 一次,其实用的很少

{

}

举个例子:

public class User {

    static {
        System.out.println("I am a static code  block!");
    }

    {
        System.out.println("I am a instance code block!");
    }
    
    public static void main(String[] args) {
        new User();
        new User();
    }

}

结果:
I am a static code  block!
I am a instance code block!
I am a instance code block!

# (3)字节码分析

我们简单分析一下字节码:

D:\code'>javap -v User.class
Classfile /D:/code'/User.class
  Last modified 2021-8-22; size 556 bytes
  MD5 checksum 10a166e49910fafcc02f1bc4ea28e055
  Compiled from "User.java"
public class User
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #8.#18         // java/lang/Object."<init>":()V
   #2 = Fieldref           #19.#20        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #21            // I am a instance code block!
   #4 = Methodref          #22.#23        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #24            // User
   #6 = Methodref          #5.#18         // User."<init>":()V
   #7 = String             #25            // I am a static code  block!
   #8 = Class              #26            // java/lang/Object
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               main
  #14 = Utf8               ([Ljava/lang/String;)V
  #15 = Utf8               <clinit>
  #16 = Utf8               SourceFile
  #17 = Utf8               User.java
  #18 = NameAndType        #9:#10         // "<init>":()V
  #19 = Class              #27            // java/lang/System
  #20 = NameAndType        #28:#29        // out:Ljava/io/PrintStream;
  #21 = Utf8               I am a instance code block!
  #22 = Class              #30            // java/io/PrintStream
  #23 = NameAndType        #31:#32        // println:(Ljava/lang/String;)V
  #24 = Utf8               User
  #25 = Utf8               I am a static code  block!
  #26 = Utf8               java/lang/Object
  #27 = Utf8               java/lang/System
  #28 = Utf8               out
  #29 = Utf8               Ljava/io/PrintStream;
  #30 = Utf8               java/io/PrintStream
  #31 = Utf8               println
  #32 = Utf8               (Ljava/lang/String;)V
{
  public User();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         // 我们发现构造器内出现了实例代码块的内容,
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc           #3                  // String I am a instance code block!
         9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: return
      LineNumberTable:
        line 1: 0
        line 8: 4
        line 9: 12

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: new           #5                  // class User
         3: dup
         4: invokespecial #6                  // Method "<init>":()V
         7: pop
         8: new           #5                  // class User
        11: dup
        12: invokespecial #6                  // Method "<init>":()V
        15: pop
        16: return
      LineNumberTable:
        line 12: 0
        line 13: 8
        line 14: 16

  // static会在一个类加载到内存的时候调用
  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #7                  // String I am a static code  block!
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 4: 0
        line 5: 8
}
SourceFile: "User.java"

# (4)执行顺序

public class Father {

    public Father(){
        
        System.out.println("这是父类的构造器!");
    }

    {
        System.out.println("这是父类的实例代码快!");
    }

    static {
        System.out.println("这是父类的静态代码快!");
    }
}
public class Son extends Father {

    public Son(){
        System.out.println("这是子类的构造器!");
    }

    {
        System.out.println("这是子类的实例代码快!");
    }

    static {
        System.out.println("这是子类的静态代码快!");
    }

    public static void main(String[] args) {
        new Son();
    }
}

结果:记住

  1. 这是父类的静态代码块!
  2. 这是子类的静态代码块!
  3. 这是父类的实例代码块!
  4. 这是父类的构造器!
  5. 这是子类的实例代码块!
  6. 这是子类的构造器!

还是分析字节码:

我们直接看Son类就可以了:

D:\code\test\out\production\test\aaa>javap -v Son.class
Classfile /D:/code/test/out/production/test/aaa/Son.class
  Last modified 2021-8-23; size 703 bytes
  MD5 checksum ec5d9dd441d9b44af4f8ae995810196c
  Compiled from "Son.java"
public class aaa.Son extends aaa.Father
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #9.#24         // aaa/Father."<init>":()V
  // 中间的省略了......
  #39 = Utf8               (Ljava/lang/String;)V
{
  public aaa.Son();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         // 子类的构造器会首先调用父类的构造器
         1: invokespecial #1                  // Method aaa/Father."<init>":()V
         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         // 从这个字节码中我们就能看出,实例代码快在子类构造器器中的代码之前    
         7: ldc           #3                  // String 这是子类的实例代码快!
         9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        15: ldc           #5                  // String 这是子类的构造器!
        17: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        20: return
      LineNumberTable:
        line 4: 0
        line 9: 4
        line 5: 12
        line 6: 20
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      21     0  this   Laaa/Son;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: new           #6                  // class aaa/Son
         3: dup
         // 调用子类构造器
         4: invokespecial #7                  // Method "<init>":()V
         7: pop
         8: return
      LineNumberTable:
        line 17: 0
        line 18: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         // 加载后会调用静态代码块                            
         3: ldc           #8                  // String 这是子类的静态代码快!
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 13: 0
        line 14: 8
}
SourceFile: "Son.java"

作业:有兴趣的同学可以在父子类中加上几个重载的方法,看看具体的调用顺序,其实这个还是挺重要的。

# 2、静态方法和静态变量

public class User {

    public static String name1 = "ydlclass";
    public String name2 = "ydlclass";

    public static void print(){
        System.out.println("hello");
    }

    public static void main(String[] args) {
        System.out.println(User.name);
        User.print();
    }

}
D:\code'>javap -v User.class
Classfile /D:/code'/User.class
  Last modified 2021-8-22; size 636 bytes
  MD5 checksum e0b2ffbf845e63ade74452ac4d383a9e
  Compiled from "User.java"
public class User
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #10.#24        // java/lang/Object."<init>":()V
   #2 = String             #25            // ydlclass
   #3 = Fieldref           #9.#26         // User.name2:Ljava/lang/String;
   #4 = Fieldref           #27.#28        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = String             #29            // hello
   #6 = Methodref          #30.#31        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #7 = Fieldref           #9.#32         // User.name1:Ljava/lang/String;
   #8 = Methodref          #9.#33         // User.print:()V
   #9 = Class              #34            // User
  #10 = Class              #35            // java/lang/Object
  #11 = Utf8               name1
  #12 = Utf8               Ljava/lang/String;
  #13 = Utf8               name2
  #14 = Utf8               <init>
  #15 = Utf8               ()V
  #16 = Utf8               Code
  #17 = Utf8               LineNumberTable
  #18 = Utf8               print
  #19 = Utf8               main
  #20 = Utf8               ([Ljava/lang/String;)V
  #21 = Utf8               <clinit>
  #22 = Utf8               SourceFile
  #23 = Utf8               User.java
  #24 = NameAndType        #14:#15        // "<init>":()V
  #25 = Utf8               ydlclass
  #26 = NameAndType        #13:#12        // name2:Ljava/lang/String;
  #27 = Class              #36            // java/lang/System
  #28 = NameAndType        #37:#38        // out:Ljava/io/PrintStream;
  #29 = Utf8               hello
  #30 = Class              #39            // java/io/PrintStream
  #31 = NameAndType        #40:#41        // println:(Ljava/lang/String;)V
  #32 = NameAndType        #11:#12        // name1:Ljava/lang/String;
  #33 = NameAndType        #18:#15        // print:()V
  #34 = Utf8               User
  #35 = Utf8               java/lang/Object
  #36 = Utf8               java/lang/System
  #37 = Utf8               out
  #38 = Utf8               Ljava/io/PrintStream;
  #39 = Utf8               java/io/PrintStream
  #40 = Utf8               println
  #41 = Utf8               (Ljava/lang/String;)V
{
  public static java.lang.String name1;
    descriptor: Ljava/lang/String;
    flags: ACC_PUBLIC, ACC_STATIC

  public java.lang.String name2;
    descriptor: Ljava/lang/String;
    flags: ACC_PUBLIC

  public User();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         // 把常量池中的项压入栈
         5: ldc           #2                  // String ydlclass
         // 为指定的类的实例域赋值,很明显这里就是赋值的操作
         7: putfield      #3                  // Field name2:Ljava/lang/String;
        10: return
      LineNumberTable:
        line 1: 0
        line 4: 4

  public static void print();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #5                  // String hello
         5: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 7: 0
        line 8: 8

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
         // 获取一个静态的变量
         3: getstatic     #7                  // Field name1:Ljava/lang/String;
         6: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         9: invokestatic  #8                  // Method print:()V
        12: return
      LineNumberTable:
        line 11: 0
        line 12: 9
        line 13: 12
  // 像这种静态块,只会调用一次
  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         // 把常量池中的项压入栈
         0: ldc           #2                  // String ydlclass
         // 为指定的类的静态域赋值
         2: putstatic     #7                  // Field name1:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 3: 0
}
SourceFile: "User.java"

其实,为什么要有构造方法,我觉得大家可以从这里看出来,即使我们的构造器是个空方法,经过编译也会将一部分对实例对象的初始化工作编译在我们的构造器中。

通过分析字节码,我们大概了解到:

1、静态的变量或者静态方法是存在方法区的,其他的方法也是在方法区(永久带,元空间)。

2、它们不属于实例对象,只是存在与方法区,调用要使用User. print(),也就是类名.方法的方式调用。


实例方法和静态方法互相调用。

1、静态方法中可以直接调用实例方法吗?

2、实例方法中可以直接调用静态方法吗?

其实我们只要明白一个道理就行,

在java中调用实例方法,必须要有主体,方法不是一等公民,不能直接当参数,也不能直接调用:

在同一个类中直接调用时默认省略了this,而在静态方法中没有this,所以在静态方法中调用实例方法,必须new一个对象。

而静态方法无论在哪里都是 类名.方法名 调用,当然同一个类的静态方法之间调用可以省略类名,不过建议还是写上。

利用静态方法和静态变量的特点,在项目中我们一般会这样使用

  • 使用静态方法完成一些工具性质的类:

    public class Computer {
    
        public static int plus(int i,int j){
            return i + j;
        }
    
        public static int minus(int i,int j){
            return i - j;
        }
    }
    

    怎么使用呢?

    Computer.plus(3,2);
    
  • 使用静态变量完成一些全局只有一份的常量类的定义,也叫静态常量。

    public class Constants {
        /**
        * UTF-8 字符集
        */
        public static final String UTF8 = "UTF-8";
    
        /**
        * GBK 字符集
        */
        public static final String GBK = "GBK";
        /**
        * 通用成功标识
        */
        public static final String SUCCESS = "0";
    
        /**
        * 通用失败标识
        */
        public static final String FAIL = "1";
        /**
        * 系统是
        */
        public static final Long SYSTEM_IS = 1L;
    
        /**
        * 系统否
        */
        public static final Long SYSTEM_NO = 0L;
    }
    

    我们怎么使用呢?

    Constants.FAIL
    

# 3、内部类

内部类: 所谓内部类就是在一个类内部进行其他类结构的嵌套操作,什么情况下有这个需求呢,回顾我们的超级链表,Node这个节点类其实主要就是给,SuperLinked使用,所以我们可以把这个类定在定SuperLinked中就好了。

class SuperLinked{
    
    Node head = null;
    //...
    //定义一个内部类
    class Node{
        
    }
}

内部类一样可以被权限修饰符来修饰,如果一个类归属于一个工程,而一个内部类就归属于一个类:

在外部如何创建内部类:

public class Outer {
    
    static {
        System.out.println("外部类被加载");
    }

    // 不被static修饰就是属于实例对象
    class Inner{
        {
            System.out.println("内部类被加载");
        }
    }

    public static void main(String[] args) {
        // 这个写法就离谱
        new Outer().new Inner();
    }
}

因为一个类内部的属性方法不加static就是属于实例对象的。

如果我们想这样更友好的创建呢?你一定能想到静态:

public class Outer {

    static {
        System.out.println("外部类被加载");
    }
    
    static class Inner{
        static {
            System.out.println("内部类被加载");
        }
    }

    public static void main(String[] args) {
        new Outer.Inner();
    }
}

外部类被加载
内部类被加载

这叫静态内部类,相比实例内部类,我们主要使用的是静态内部类。

当然如果我们不希望其他的类访问我们的内部类,加上private就好了。

# 4、单例设计模式

单例模式,是一种常用的软件设计模式。通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例。即一个类只有一个对象实例。

具体的思路是:

(1)别人不能new实例,所以要将构造方法私有化,使其不能在类的外部通过new关键字实例化该类对象。

(2)在该类内部产生一个唯一的实例化对象,并且将其封装为private static类型。

(3)定义一个静态方法返回这个唯一对象。

# (1)饿汉式

立即加载就是使用类的时候已经将对象创建完毕(不管以后会不会使用到该实例化对象,先创建了再说。很着急的样子,故又被称为“饿汉模式”),常见的实现办法就是直接new实例化。

public class Singleton {

    // 将自身实例化对象设置为一个属性,并用static、final修饰
    private static final Singleton instance = new Singleton();
    
    // 构造方法私有化
    private Singleton() {}
    
    // 静态方法返回该实例
    public static Singleton getInstance() {
        return instance;
    }
}

# (2)懒汉式

延迟加载就是调用get()方法时实例才被创建(先不急着实例化出对象,等要用的时候才给你创建出来。不着急,故又称为“懒汉模式”),常见的实现方法就是在get方法中进行new实例化。

public class Singleton {

    // 将自身实例化对象设置为一个属性,并用static修饰
    private static Singleton instance;
    
    // 构造方法私有化
    private Singleton() {}
    
    // 静态方法返回该实例
    public static Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

这种懒汉式在多线程环境中是完全错误的,根本不能保证单例的状态,我们目前先不用理解。后边会详细介绍。

# (3)内部类实现单例

这种也是懒汉式的一种实现,而且使用这种懒汉式没有任何的线程问题,大家来思考,结合咱们上边的内容,只要不调用getInstance()方法,就不会使用内部类,内部类一旦被使用只会被初始化一次,以后一直用的就是静态常量 INSTANCE 了。

public class Singleton {

    /** 私有化构造器 */
    private Singleton() {
    }

    /** 对外提供公共的访问方法 */
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }

    /** 写一个静态内部类,里面实例化外部类 */
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

}

# 4、匿名内部类

匿名内部类可以使你的代码更加简洁,你可以在定义一个类时同时对其进行实例化。它与局部类很相似,不同的是它没有类名,如果某个局部类你只需要用一次,那么你就可以使用匿名内部类。

首先看下官方文档中给的例子:

如上文所述,匿名类是一个表达式,匿名类的语法就类似于调用一个类的构建函数(new HelloWorld()),除这些之外,还包含了一个代码块,在代码块中完成类的定义,见以下两个实例:

实现接口的匿名类

wolf.eat(new Animal() {
            @Override
            public void eat(Animal animal) {
                System.out.println(this.getName() + "吃了" + animal.getName());
            }

            @Override
            public String getName() {
                return "pig";
            }
        });

之前的列子,常理中接口是不能直接new的。

匿名子类(继承父类)

tiger.eat(new Wolf(){
            @Override
            public void eat(Animal animal) {
                super.eat(animal);
            }

            @Override
            public String getName() {
                return "白狼王";
            }
        });

从以上两个实例中可知,匿名类表达式包含以下内部分:

  1. 操作符:new;
  2. 一个要实现的接口或要继承的类,案例一中的匿名类实现了HellowWorld接口,案例二中的匿名内部类继承了Animal父类;
  3. 一对括号,如果是匿名子类,与实例化普通类的语法类似,如果有构造参数,要带上构造参数;如果是实现一个接口,只需要一对空括号即可;
  4. 一段被"{}"括起来类声明主体;
  5. 末尾的";"号(因为匿名类的声明是一个表达式,是语句的一部分,因此要以分号结尾)。

回头思考我们的策略设计模式中,就可以使用匿名内部类实现,直接在参数上new Comparator(){}就可以了。

【内部类编译出来后是一个类还是两个呢?】

public class Dog  {

    public static void main(String[] args) {
        
        String name = "teddy";
        
        Animal animal = new Animal(){
            @Override
            public void eat(Animal animal) {
                System.out.println(name + "is eating!");
            }

            @Override
            public String getName() {
                // 这里会报错
                name = "hashiqi";
                return name;
            }
        };
        animal.eat(animal);
    }
}

首先这个问题会出错。

# 5、箭头函数

如果一个接口只有一个抽象方法,那么这个接口也称之为函数式接口。可以使用@FunctionalInterface注解标识。

@FunctionalInterface
public interface Function {
    int plus(int i,int j);
}

当我们使用函数式接口去构造内部类时,我们很简单的表示:

public class Client {

    /**
     * 这个类能计算加法
     * @param function
     */
    public static void test(Function function){
        System.out.println(function.plus(1, 2));
    }

    public static void main(String[] args) {
        // 这里我们使用了内部类
        test(new Function() {
            @Override
            public int plus(int i, int j) {
                return i + j;
            }
        });
    }
}

我们可以对之进行简化,

1、类名方法名全不要,这个结构分为两部分,第一部分,小括号包裹形参,类型也不要、第二部分 【->】、第三部分是方法体:

 public static void main(String[] args) {
        // 这里我们使用了内部类
        test((i,j) -> {return i + j;});

    }

2、方法体如果只用一行代码,大括号可以省略,如果一行代码是返回值,return可以省略。

 public static void main(String[] args) {
        // 这里我们使用了内部类
        test((i,j) -> i + j);

    }

更多的内容我们放在函数式编程中讲。

# 6、值传递和所谓的引用传递

本质上java只有值传递,所有的传递都是一次值的拷贝。引用类型拷贝的是引用地址,基础数据类型拷贝的是值,不会传入实例对象本身。

我们先用一个例子热热身,你觉得最后的结果是什么呢?

public class Dog {

    private String name;

    public Dog(String name) {
        this.name = name;
    }

    public static void changeDog(Dog dog){
        dog = new Dog("jerry");
    }

    public static void main(String[] args) {
        Dog dog = new Dog("tom");
        changeDog(dog);
        System.out.println(dog);

    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                '}';
    }
}

image-20210825173007972image-20210825173007972

我们用下边三个例子,给大家详细的介绍一下我们遇到的各种值传递的问题:

1、对于基础数据类型,调用changeInt(i),这个过程不是把【i】传入这个方法,而是将i的值也就是【5】拷贝一份,赋值给形参【j】,所以形参无论怎么操作,不会影响【i】

changeInt(int j){
	j = 8;
}

int i = 5;
changeInt(i);
System.out.println(i);

结果:
    5

2、对于引用数据类型,调用changeInt(dog)方法,也不是将【dog】传入方法,而是将dog的引用地址值(0x123FE222)拷贝一份赋值给形参【d】,名字一样不一样都无所谓,当d = new Dog("tom");执行时,形参会开辟新空间,指向新对象,外部的【dog】不受影响。

changeInt(Dog d){
	d = new Dog("tom");
}

Dog dog = new Dog("jerry");
changeInt(dog);
System.out.println(dog.getName());

结果:
jerry

3、下边这种情况是另外一种情况,调用changeInt(dog)方法,当然也不是将【dog】传入方法,而是将dog的引用地址值(0x123FE222)拷贝一份赋值给形参【d】,此时无论是【形参d】还是外部【引用dog】都指向同一个地址的实例对象,内部使用d.setName("tom");修改实例对象当然会印象dog所指向的实例,因为是同一个嘛。

changeInt(Dog d){
	d.setName("tom");
}

Dog dog = new Dog("jerry");
changeInt(dog);
System.out.println(dog.getName());

结果:
tom

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值