• jhat 内存分析工具

  • JDK > JRE > JVM

  • 多行注释 /* /
    文档注释 /
    * */

  • 字面量 aka 常量 aka 字面值常量

  • 空类型 一个特殊的值 aka 空值 aka null

  • IDEA项目结构:project > module > package > class

  • 在代码中,如果有小数参与计算,结果有可能不精确。(精确要用BigDecima包)


数据类型转换

数字进行运算时,如果数据类型不一样,要转成一样的,才能运算。

隐式转换 aka 自动类型提升:取值范围小 => 大

取值范围:byte < short < int < long < float < double
隐式转换的两种提升规则:

  1. 取值范围小的和取值范围大的进行运算,小的会先提升为大的
  2. byte short char 三种类型的数据在运算时,都会先直接提升为int

强制转换:取值范围大 => 小

如果把一个取值范围大的数值赋值给取值范围小的变量,一定要这么做就要加入强制转换。
用法略。

注意:这样做数据容易发生错误。


  • 原码 + 反码 + 补码

基本数据类型

byte 1个字节 10 = 0000 1010(2)
short 2个字节 10 = 0000 0000 0000 1010(2)
int 4个字节 10 = 0000 0000 0000 0000 0000 0000 0000 1010(2)
long 8个字节 10 = 0000 0000 0000 0000 0000 0000 0000 0000
0000 0000 0000 0000 0000 0000 0000 1010(2)

其他运算符

& 逻辑与 同1为1
| 逻辑或 有1为1
<< 左移(一次相当于乘2) 向左移动,低位补0
>> 右移(一次相当于除2) 向右移动,高位补0或1(根据原本数字的正负)
>>> 无符号右移 向右移动,高位补0

数组

初始化:在内存中为数组开辟空间,并将数据存入容器的过程

数组的静态初始化(手动指定数组元素,系统根据元素个数计算出数组长度)

完整格式(一般不用):int[] array = new int[]{1, 2, 3};

简化格式:int array[] = {1, 2, 3};

数组动态初始化(手动指定数组长度,由系统给出默认初始化值)

推荐在“只明确元素个数,不明确具体数值”时使用动态初始化。

int[] array = new int[数组长度]

在创建数组时,我们可以自己指定数组的长度,由虚拟机给出默认的初始化值

各种类型的数组的默认初始化值:
整数类型:0
小数类型:0.0
字符类型:’/u0000’空格
布尔类型:false
引用类型(类、接口、数组、String):null

扩展:地址的格式含义

例如一个double类型数组的地址[D@776ec8df
[ 表示当前是一个数组
D 表示当前数组里面的元素都是double类型的
@ 固定的符号,后面接着的是真正意义上的地址值
不过一般习惯性地将这个整体叫做数组的地址值。

  • 索引 aka 下标

Java内存分配

  • 方法运行时使用的内存,比如main方法运行,进入方法栈中运行
  • 存储对象或数组,通过new创建的,都存储在堆内存
  • 方法区 存储可以运行的class文件
  • 本地方法栈 JVM在使用操作系统功能时使用,与我们开发无关
  • 寄存器 给CPU使用,与我们开发无关

方法

方法(method)是Java程序中最小的执行单位。
提高代码的复用性、可维护性。

重载

同一个类中,方法名相同,参数不同的方法。与返回值无关。
参数不同:个数不同、类型不同、顺序不同。
注意:不同类型参数的顺序不同也可以构成重载,但是不建议!

方法的基本内存原理

栈,FIFO

基本数据类型和引用数据类型

基本数据类型

变量中存储的是真实的数据。
从内存的角度解释:数据值是存储在自己的空间中。
特点:赋值给其他变量,也是赋真实的值。

引用数据类型

变量中存储的是地址值。
从内存的角度解释:数据值存储在其他空间中,自己空间中存储的是地址值。
引用 aka 使用了其他空间中的数据

方法传递基本数据类型的内存原理

  • 传递基本数据类型时,传递的是真实的数据,形参的改变,不影响实际参数的值
  • 传递引用数据类型时,传递的是地址值,形参的改变,影响实际参数的值

如何定义类

1
2
3
4
5
6
7
public class 类名 {
1. 成员变量
2. 成员方法
3. 构造器
4. 代码块
5. 内部类
}
  • 一个Java文件中可以定义多个类,且只能一个类是public修饰的,而且public修饰的类的类名必须成为代码文件名。
    实际开发中建议一个文件定义一个类。

  • 成员变量的完整定义格式:修饰符 数据类型 变量名称 = 初始化值; 一般无需指定初始化值,存在默认值。

封装

  • private可修饰成员(成员变量和成员方法),被它修饰的成员只能在本类中才能访问。

  • this的作用是区别成员变量和局部变量

构造方法

aka 构造器 aka 构造函数

1
2
3
4
5
public class 类名 {
修饰符 类名(参数) {
方法体;
}
}
  • 创建对象时,虚拟机会自动调用构造方法,作用是给成员变量进行初始化。

特点:

  1. 方法名与类名完全一致
  2. 没有返回值类型,连void也没有
  3. 没有具体的返回值(不能由return带回结果数据)
    执行时机:
  4. 创建对象时由虚拟机调用,不能手动调用构造方法
  5. 每创建一次对象,就会调用一次构造方法

注意事项:构造方法的定义

  1. 如果没有定义构造方法,系统将给出一个默认的无参数构造方法。
  2. 如果定义了构造方法,系统将不再提供默认的构造方法。此时就需要自己写无参数构造器了。

注意事项:构造方法的重载

带参构造方法和无参构造方法,两者方法名相同,但是参数不同,这叫做构造方法的重载。

推荐的方式

任何时候,无论是否使用,都手动写上无参构造方法和带全部参数的构造方法。

三种情况的对象内存图

在JDK7及以前,堆和方法区是连在一起的,在真实的物理空间中也是连续的。但是这种设计方式不好,从JDK8开始便改进了,取消了方法区,新增了元空间。把原来方法区的多种功能进行拆分,有的功能放到了堆中,有的功能放到了元空间中。

Student s = new Student()

  1. 加载class文件
  2. 申明局部变量
  3. 在堆内存中开辟一个空间
  4. 默认初始化
  5. 显式初始化
  6. 构造方法初始化
  7. 将堆内存中的地址值赋值给左边的局部变量
  • 一个对象的内存图(略)
  • 两个对象的内存图(略)
  • 两个引用指向同一个对象的内存图(略)

this的内存原理

this的作用:区分局部变量和成员变量
this的本质:代表方法调用者的地址值

区别 成员变量 局部变量
类中位置不同 类中,方法外 方法内、方法声明上
初始化值不同 有默认初始化值 没有,使用之前需要完成赋值
内存位置不同 堆内存 栈内存
生命周期不同 随着对象的创建而存在,随着对象的消失而消失 随着方法的调用而存在,随着方法的运行结束而消失
作用域 整个类中有效 当前方法中有效

String

  • String是Java定义好的一个类。定义在java.lang包中,所以使用的时候不需要导包。
  • Java程序中的所有字符串文字,都被视为此类的对象。
  • 字符串不可变,它们的值在创建后不能被更改。

创建String对象的两种方式

  1. 直接赋值

String name = "haifan16";

  1. new
    image-20230920011955062

内存模型

注意:从JDK7开始,StringTable(串池)从方法区挪到了堆内存。
image-20230920011904841

image-20230920011918000

当使用双引号直接赋值时,系统会检查该字符串在串池中是否存在:
不存在 -> 创建新的
存在 -> 复用

StringBuilder

可以看成是一个可变的容器,创建之后里面的内容是可变的。
提高字符串的操作效率。

  • 主要应用场景:拼接字符串;反转字符串。

常用方法

image-20230920012034121

StringJoiner

StringJoiner跟StringBuilder一样,也可以看成是一个容器,创建之后里面的内容是可变的。
提高字符串的操作效率,而且代码编写非常简洁,但是很少有人用。(JDK8出现的)

字符串相关类的底层原理

  1. 字符串存储的内存原理
  • 直接赋值会复用字符串常量池(aka 串池)中的
  • new出来不会复用,而是开辟一个新的空间
  1. ==号比较的是什么?

基本数据类型:比较的是数据值。
引用数据类型:比较的是地址值。

  • 因此,比较字符串的内容要用的是equals方法 / equalsIgnoreCase方法。
  1. 字符串拼接的底层原理
  • 如果没有变量参与,都是字符串直接相加,编译之后就是拼接之后的结果,会复用串池中的字符串。
  • 如果有变量参与,会创建新的字符串,浪费内存。(所以拼接字符串尽量不要直接+,而是用StringBuilder / StringJoiner)
  1. StringBuilder提高效率原理图
  • 所有要拼接的内容都会往StringBuilder中放,不会创建很多无用的空间,节约内存。
  1. StringBuilder源码分析
  • 默认创建一个长度为16的字节数组。
  • 添加的内容长度≤16,直接存。
  • 添加的内容>16会扩容至【原来的容量 * 2 + 2】
  • 如果扩容之后还不够(长度大于70以后),以实际的长度为准。

集合ArrayList

数组 vs 集合

数组长度固定不变,可存基本数据类型和引用数据类型。
集合长度可变,可存引用数据类型,可用包装类间接存基本数据类型。

static修饰符

可修饰成员方法,成员变量。

静态变量(被static修饰的成员变量)

特点:
1. 被该类所有对象共享。
2. 不属于对象,属于类。
3. 随着类的加载而加载,优先于对象存在。

调用方式:
1. 类名调用(推荐)
2. 对象名调用

静态方法(被static修饰的成员方法)

特点:

  1. 多用在测试类和工具类中
  2. JavaBean类中很少会用

调用方式:
1. 类名调用(推荐)
2. 对象名调用

static的注意事项

  1. 静态方法中,只能访问静态。

  2. 非静态方法可以访问所有。

  3. 静态方法中没有this关键字。

static内存图

image-20230920012134017


  • JavaBean类:用来描述一类事物的类
  • 测试类:用来检查其他类是否书写正确,带有main方法的类,是程序的入口
  • 工具类:不是用来描述一类事物的,而是帮我们做一些事情的类

写工具类的规则:

  1. 类名见名知意
  2. 私有化构造方法
  3. 方法定义为静态

  • 单例设计模式 -> 多线程阶段讲解。

重新认识main方法

  • public:被JVM调用,访问权限足够大。
  • static:被JVM调用,不用创建对象,直接类名访问。
    因为main方法是静态的,所以测试类中其他方法也需要是静态的。
  • void:被JVM调用,不需要给JVM返回值。
  • main:一个通用的名称,虽然不是关键字,但是被JVM识别。
  • String[] args:一起拿用于接收键盘录入数据的,现在没用。

继承

子类 aka 派生类;父类 aka 基类 aka 超类

使用继承的可以把多个子类中重复的代码抽取到父类中,提高代码的复用性。

继承后子类的特点:

  1. 子类可以得到父类的属性和行为,子类可以使用。
  2. 子类可以在父类的基础上增加其他功能,使子类更强大。

继承的特点:

  1. Java只能单继承,不能多继承,但是可以多层继承。
  2. Java中所有的类都直接或间接继承于Object类。
  3. 子类只能访问父类中非私有的成员。

子类能继承父类中的哪些内容(内存图 / 内存分析工具)

非private private
构造方法 不能 不能
成员变量
成员方法 不能

内存图

image-20230920012455677

虚方法表(非privare,非static,非final)

image-20230920013053035

只有父类中的虚方法才能被子类继承。

image-20230920013427572

继承中成员方法的访问特点

  1. this调用:就近原则
  2. super调用:直接找父类

方法重写

在继承体系中,子类出现了和父类中一模一样的方法声明,我们就称子类的这个方法是重写的方法。

@Override注解

方法重写建议加上@Override注解,可以校验重写是否正确,同时可读性好。

重写方法的基本要求

  1. 子类重写的方法尽量跟父类中的方法保持一致。
  2. 只有虚方法表里面的方法可以被重写。

方法重写的本质

是覆盖虚方法表中的方法。

super关键字

多态

多态:同类型的对象表现出的不同形态。

表现形式

1
父类类型 对象名称 = 子类对象;

多态的前提

  • 有继承 / 实现关系
  • 有父类引用指向子类对象
  • 有方法重写

多态的好处

使用父类型作为参数,可以接收所有子类对象,体现多态的扩展性与便利。

多态调用成员的特点

  • 变量调用:编译看左边,运行也看左边
  • 方法调用:编译看左边,运行看右边

多态的优势和弊端

优势:

  1. 在多态形式下,右边对象可以实现解耦合,便于扩展和维护。
  2. 定义方法时,使用父类型作为参数,可以接收所有子类对象,体现多态的扩展性与便利。

弊端:

  • 不能调用子类的特有功能。

引用数据类型的类型转换,有几种方式?

  1. 自动类型转换
  2. 强制类型转换

  • instanceof

强制类型转换能解决什么问题?

  1. 可以转换成真正的子类类型,从而调用子类独有功能。
  2. 转换类型与真实对象类型不一致会报错。
  3. 转换时最好用instanceof关键字进行判断。

包就是文件夹,用来管理不同功能的各种类。

包名的书写规则:公司域名反写 + 包的作用,全英文小写。

全类名:包名 + 类名

什么时候导包 / 不导包:

  1. 使用同一个包中的类,不用导包。
  2. 使用java.lang包中的类,不用导包。
  3. 其他情况需要导包。
  4. 同时使用两个包中的同名类,需要用全类名。

final关键字

可以修饰方法、类、变量。
修饰方法:表明该方法是最终方法,不能被重写。
修饰类:表明该类是最终类,不能被继承。
修饰变量:叫做常量,只能被赋值一次。

权限修饰符

用来控制一个成员能够被访问的范围。
可以修饰成员变量、方法、构造方法、内部类。

image-20230920231658944

权限修饰符的使用规则:

实际开发中,一般只用private和public。

  • 成员变量私有
  • 方法公开
    特例:如果方法中的代码是抽取其他方法中共性的代码,这个方法一般也私有。(?)

代码块

代码块分为3种:局部代码块、构造代码块、静态代码块

局部代码块

提前结束变量的生命周期(已淘汰)

构造代码块

写在成员位置的代码块。
作用是抽取多个构造方法中重复的代码。在创建本类对象时会先执行构造代码块,再执行构造方法。缺点是不够灵活,因此了解即可。

静态代码块(重要)

格式:

1
static {}

特点:通过需要static关键字修饰,随着类的加载而加载,并且自动触发,只执行一次。
使用场景:在类加载时,做一些数据初始化的时候。

抽象

  • 抽象方法:将共性的行为(方法)抽取到父类之后,由于每一个子类执行的内容是不一样的,所以在父类中不能确定具体的方法体。该方法就可以定义为抽象方法。

  • 抽象类:如果一个类中存在抽象方法,该类就必须声明为抽象类。
    作用:抽取共性时,无法确定方法体,就把方法定义为抽象的,强制让子类按照某种格式重写。

抽象类和抽象方法的注意事项:

  1. 抽象类不能实例化
  2. 抽象类中不一定有抽象方法,有抽象方法一定是抽象类
  3. 可以有构造方法
  4. 抽象类的子类:要么重写抽象类的所有抽象方法(更常见);要么是抽象类。
  • 抽象类和抽象方法的意义(略)

接口

  • 接口的意义(略)

接口不能实例化。

接口和类之间是实现关系,通过implements关键字表示。

接口的子类(实现类):要么重写接口中的所有抽象方法(更常见);要么是抽象类。

注意:

  1. 接口和类的实现关系,可以单实现,也可以多实现:
    1
    public class 类名 implements 接口名1, 接口名2 {}
  2. 实现类还可以在继承一个类的同时实现多个接口:
    1
    public class 类名 extends 父类 implements 接口名1, 接口名2 {}

接口中成员的特点

  1. 成员变量:
    只能是常量。
    默认修饰符:public static final
  2. 没有构造方法。
  3. 成员方法:
    只能是抽象方法。
    默认修饰符:public abstract

接口、类的各种关系

  1. 类和类的关系:继承关系,只能单继承,不能多继承,但是可以多层继承。
  2. 类和接口的关系:实现关系,可以单实现,也可以多实现,还可以在继承一个类的同时实现多个接口。
  3. 接口和接口的关系:继承关系,可以单继承,也可以多继承。
    细节:如果实现类实现类最下面的子接口,那么就要重写所有的抽象方法。

接口中新增的方法

  • JDK7及以前:接口中只能定义抽象方法。
  • JDK8新特性:接口中可以定义有方法体的方法。(默认、静态)
  • JDK9新特性:接口中可以定义私有方法。

接口中的默认方法(JDK8开始)

  • 允许在接口中定义默认方法,需要使用关键字default修饰。
    作用:解决接口升级的问题

注意事项:

  1. 默认方法不是抽象方法,所以不强制被重写。但是如果被重写,重写的时候去掉default关键字。
  2. public可以省略,default不能省略。
  3. 如果实现了多个接口,多个接口中存在相同名字的默认方法,子类就必须对该方法进行重写。

接口中的静态方法(JDK8开始)

  • 允许在接口中定义静态方法,用static修饰。

注意事项:

  1. 静态方法只能通过接口名调用,不能通过实现类名或对象名调用。
  2. public可以省略,static不能省略。

接口中的私有方法(JDK9开始)

分为两种:

  • 普通的私有方法(为默认方法服务的)
  • 静态的私有方法(为静态方法服务的)

接口的应用

  1. 接口代表规则,是行为的抽象。想让一个类拥有某个行为,让它实现对应的接口就可以了。
  2. 当一个方法的参数是接口时,可以传递接口所有实现类的对象,这种方式叫接口多态

适配器设计模式

解决接口与接口实现类之间的矛盾问题。

  1. 当一个接口中抽象方法过多,但是我只要使用其中一部分时,就可以使用适配器设计模式。
  2. 操作过程:写中间类xxxAdapter,实现对应的接口,对接口中的抽象方法进行空实现,再让真正的实现类继承这个中间类,并重写需要用的方法。另外,为了避免其他类创建适配器类的对象,中间的适配器类要用abstact修饰。
  3. 如果实现类本身需要继承某一个父类,也很好解决——让这个中间类继承那个父类就行。

内部类

  • 成员内部类
  • 静态内部类
  • 局部内部类
  • 匿名内部类

内存类的内存图

image-20230921144100085

成员内部类

  • 就是写在成员位置的属于外部类的成员。
  • 可以被一些修饰符修饰,比如private, 默认, proteted, public, static(静态内部类)等。
  • JDK16开始,可以在成员内部类里面定义静态变量,之前不可以。

获取成员内部类对象

  1. 方式一(当成员内部类被private修饰时):在外部类中编写方法,对外提供内部类对象
  2. 方式二(当成员内部类被非private修饰时):直接创建对象。
    格式:
    1
    Outer.Inner oi = new Outer().new Inner();
  3. 外部类成员变量和内部类成员变量重名时,在内部类如何访问?
    1
    System.out.println(Outer.this.变量名);

静态内部类

是一种特殊的成员内部类。
静态内部类只能访问外部类中的静态变量和静态方法,如果想要访问非静态的,需要创建对象。

创建静态内部类对象的格式:

1
Outer.Inner oi = new Outer().Inner();

调用非静态方法的格式:先创建对象,再用对象调用。
调用静态方法的格式:不需要创建对象,直接Outer.Inner.method();

局部内部类

  1. 将内部类定义在方法中,就叫做局部内部类,类似方法中的局部变量。
  2. 外界无法直接使用,需要在方法内部创建对象并使用。
  3. 该类可以直接访问外部类的成员,也可以访问方法内的局部变量。

匿名内部类(重要)

本质上就是隐藏了名字的内部类。它可以写在成员位置,也可以写在局部位置。

格式:

1
2
3
new 类名或接口名() {
重写方法;
}

格式的细节:包含了实现关系、方法的重写、创建对象。
从语法的角度看,它整体就是一个类的子类对象或一个接口的实现类对象。

使用场景:当方法的参数是接口或类时。以接口为例,可以传递这个接口的实现类对象。如果实现类只要使用一次,就可以用匿名内部类简化代码。