【自用】JavaSE学习笔记(一)
jhat 内存分析工具
JDK > JRE > JVM
多行注释 /* /
文档注释 /* */字面量 aka 常量 aka 字面值常量
空类型 一个特殊的值 aka 空值 aka null
IDEA项目结构:project > module > package > class
在代码中,如果有小数参与计算,结果有可能不精确。(精确要用BigDecima包)
数据类型转换
数字进行运算时,如果数据类型不一样,要转成一样的,才能运算。
隐式转换 aka 自动类型提升:取值范围小 => 大
取值范围:byte < short < int < long < float < double
隐式转换的两种提升规则:
- 取值范围小的和取值范围大的进行运算,小的会先提升为大的
- 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 | public class 类名 { |
一个Java文件中可以定义多个类,且只能一个类是public修饰的,而且public修饰的类的类名必须成为代码文件名。
实际开发中建议一个文件定义一个类。成员变量的完整定义格式:修饰符 数据类型 变量名称 = 初始化值; 一般无需指定初始化值,存在默认值。
封装
private可修饰成员(成员变量和成员方法),被它修饰的成员只能在本类中才能访问。
this的作用是区别成员变量和局部变量
构造方法
aka 构造器 aka 构造函数
1 | public class 类名 { |
- 创建对象时,虚拟机会自动调用构造方法,作用是给成员变量进行初始化。
特点:
- 方法名与类名完全一致
- 没有返回值类型,连void也没有
- 没有具体的返回值(不能由return带回结果数据)
执行时机: - 创建对象时由虚拟机调用,不能手动调用构造方法
- 每创建一次对象,就会调用一次构造方法
注意事项:构造方法的定义
- 如果没有定义构造方法,系统将给出一个默认的无参数构造方法。
- 如果定义了构造方法,系统将不再提供默认的构造方法。此时就需要自己写无参数构造器了。
注意事项:构造方法的重载
带参构造方法和无参构造方法,两者方法名相同,但是参数不同,这叫做构造方法的重载。
推荐的方式
任何时候,无论是否使用,都手动写上无参构造方法和带全部参数的构造方法。
三种情况的对象内存图
在JDK7及以前,堆和方法区是连在一起的,在真实的物理空间中也是连续的。但是这种设计方式不好,从JDK8开始便改进了,取消了方法区,新增了元空间。把原来方法区的多种功能进行拆分,有的功能放到了堆中,有的功能放到了元空间中。
Student s = new Student()
- 加载class文件
- 申明局部变量
- 在堆内存中开辟一个空间
- 默认初始化
- 显式初始化
- 构造方法初始化
- 将堆内存中的地址值赋值给左边的局部变量
- 一个对象的内存图(略)
- 两个对象的内存图(略)
- 两个引用指向同一个对象的内存图(略)
this的内存原理
this的作用:区分局部变量和成员变量
this的本质:代表方法调用者的地址值
区别 | 成员变量 | 局部变量 |
---|---|---|
类中位置不同 | 类中,方法外 | 方法内、方法声明上 |
初始化值不同 | 有默认初始化值 | 没有,使用之前需要完成赋值 |
内存位置不同 | 堆内存 | 栈内存 |
生命周期不同 | 随着对象的创建而存在,随着对象的消失而消失 | 随着方法的调用而存在,随着方法的运行结束而消失 |
作用域 | 整个类中有效 | 当前方法中有效 |
String
- String是Java定义好的一个类。定义在java.lang包中,所以使用的时候不需要导包。
- Java程序中的所有字符串文字,都被视为此类的对象。
- 字符串不可变,它们的值在创建后不能被更改。
创建String对象的两种方式
- 直接赋值
String name = "haifan16";
- new
内存模型
注意:从JDK7开始,StringTable(串池)从方法区挪到了堆内存。
当使用双引号直接赋值时,系统会检查该字符串在串池中是否存在:
不存在 -> 创建新的
存在 -> 复用
StringBuilder
可以看成是一个可变的容器,创建之后里面的内容是可变的。
提高字符串的操作效率。
- 主要应用场景:拼接字符串;反转字符串。
常用方法
StringJoiner
StringJoiner跟StringBuilder一样,也可以看成是一个容器,创建之后里面的内容是可变的。
提高字符串的操作效率,而且代码编写非常简洁,但是很少有人用。(JDK8出现的)
字符串相关类的底层原理
- 字符串存储的内存原理
- 直接赋值会复用字符串常量池(aka 串池)中的
- new出来不会复用,而是开辟一个新的空间
- ==号比较的是什么?
基本数据类型:比较的是数据值。
引用数据类型:比较的是地址值。
- 因此,比较字符串的内容要用的是equals方法 / equalsIgnoreCase方法。
- 字符串拼接的底层原理
- 如果没有变量参与,都是字符串直接相加,编译之后就是拼接之后的结果,会复用串池中的字符串。
- 如果有变量参与,会创建新的字符串,浪费内存。(所以拼接字符串尽量不要直接+,而是用StringBuilder / StringJoiner)
- StringBuilder提高效率原理图
- 所有要拼接的内容都会往StringBuilder中放,不会创建很多无用的空间,节约内存。
- StringBuilder源码分析
- 默认创建一个长度为16的字节数组。
- 添加的内容长度≤16,直接存。
- 添加的内容>16会扩容至【原来的容量 * 2 + 2】
- 如果扩容之后还不够(长度大于70以后),以实际的长度为准。
集合ArrayList
数组 vs 集合
数组长度固定不变,可存基本数据类型和引用数据类型。
集合长度可变,可存引用数据类型,可用包装类间接存基本数据类型。
static修饰符
可修饰成员方法,成员变量。
静态变量(被static修饰的成员变量)
特点:
1. 被该类所有对象共享。
2. 不属于对象,属于类。
3. 随着类的加载而加载,优先于对象存在。
调用方式:
1. 类名调用(推荐)
2. 对象名调用
静态方法(被static修饰的成员方法)
特点:
- 多用在测试类和工具类中
- JavaBean类中很少会用
调用方式:
1. 类名调用(推荐)
2. 对象名调用
static的注意事项
静态方法中,只能访问静态。
非静态方法可以访问所有。
静态方法中没有this关键字。
static内存图
- JavaBean类:用来描述一类事物的类
- 测试类:用来检查其他类是否书写正确,带有main方法的类,是程序的入口
- 工具类:不是用来描述一类事物的,而是帮我们做一些事情的类
写工具类的规则:
- 类名见名知意
- 私有化构造方法
- 方法定义为静态
- 单例设计模式 -> 多线程阶段讲解。
重新认识main方法
- public:被JVM调用,访问权限足够大。
- static:被JVM调用,不用创建对象,直接类名访问。
因为main方法是静态的,所以测试类中其他方法也需要是静态的。 - void:被JVM调用,不需要给JVM返回值。
- main:一个通用的名称,虽然不是关键字,但是被JVM识别。
- String[] args:一起拿用于接收键盘录入数据的,现在没用。
继承
子类 aka 派生类;父类 aka 基类 aka 超类
使用继承的可以把多个子类中重复的代码抽取到父类中,提高代码的复用性。
继承后子类的特点:
- 子类可以得到父类的属性和行为,子类可以使用。
- 子类可以在父类的基础上增加其他功能,使子类更强大。
继承的特点:
- Java只能单继承,不能多继承,但是可以多层继承。
- Java中所有的类都直接或间接继承于Object类。
- 子类只能访问父类中非私有的成员。
子类能继承父类中的哪些内容(内存图 / 内存分析工具)
非private | private | |
---|---|---|
构造方法 | 不能 | 不能 |
成员变量 | 能 | 能 |
成员方法 | 能 | 不能 |
内存图
虚方法表(非privare,非static,非final)
只有父类中的虚方法才能被子类继承。
继承中成员方法的访问特点
- this调用:就近原则
- super调用:直接找父类
方法重写
在继承体系中,子类出现了和父类中一模一样的方法声明,我们就称子类的这个方法是重写的方法。
@Override注解
方法重写建议加上@Override注解,可以校验重写是否正确,同时可读性好。
重写方法的基本要求
- 子类重写的方法尽量跟父类中的方法保持一致。
- 只有虚方法表里面的方法可以被重写。
方法重写的本质
是覆盖虚方法表中的方法。
super关键字
多态
多态:同类型的对象表现出的不同形态。
表现形式
1 | 父类类型 对象名称 = 子类对象; |
多态的前提
- 有继承 / 实现关系
- 有父类引用指向子类对象
- 有方法重写
多态的好处
使用父类型作为参数,可以接收所有子类对象,体现多态的扩展性与便利。
多态调用成员的特点
- 变量调用:编译看左边,运行也看左边
- 方法调用:编译看左边,运行看右边
多态的优势和弊端
优势:
- 在多态形式下,右边对象可以实现解耦合,便于扩展和维护。
- 定义方法时,使用父类型作为参数,可以接收所有子类对象,体现多态的扩展性与便利。
弊端:
- 不能调用子类的特有功能。
引用数据类型的类型转换,有几种方式?
- 自动类型转换
- 强制类型转换
- instanceof
强制类型转换能解决什么问题?
- 可以转换成真正的子类类型,从而调用子类独有功能。
- 转换类型与真实对象类型不一致会报错。
- 转换时最好用instanceof关键字进行判断。
包
包就是文件夹,用来管理不同功能的各种类。
包名的书写规则:公司域名反写 + 包的作用,全英文小写。
全类名:包名 + 类名
什么时候导包 / 不导包:
- 使用同一个包中的类,不用导包。
- 使用java.lang包中的类,不用导包。
- 其他情况需要导包。
- 同时使用两个包中的同名类,需要用全类名。
final关键字
可以修饰方法、类、变量。
修饰方法:表明该方法是最终方法,不能被重写。
修饰类:表明该类是最终类,不能被继承。
修饰变量:叫做常量,只能被赋值一次。
权限修饰符
用来控制一个成员能够被访问的范围。
可以修饰成员变量、方法、构造方法、内部类。
权限修饰符的使用规则:
实际开发中,一般只用private和public。
- 成员变量私有
- 方法公开
特例:如果方法中的代码是抽取其他方法中共性的代码,这个方法一般也私有。(?)
代码块
代码块分为3种:局部代码块、构造代码块、静态代码块
局部代码块
提前结束变量的生命周期(已淘汰)
构造代码块
写在成员位置的代码块。
作用是抽取多个构造方法中重复的代码。在创建本类对象时会先执行构造代码块,再执行构造方法。缺点是不够灵活,因此了解即可。
静态代码块(重要)
格式:
1 | static {} |
特点:通过需要static关键字修饰,随着类的加载而加载,并且自动触发,只执行一次。
使用场景:在类加载时,做一些数据初始化的时候。
抽象
抽象方法:将共性的行为(方法)抽取到父类之后,由于每一个子类执行的内容是不一样的,所以在父类中不能确定具体的方法体。该方法就可以定义为抽象方法。
抽象类:如果一个类中存在抽象方法,该类就必须声明为抽象类。
作用:抽取共性时,无法确定方法体,就把方法定义为抽象的,强制让子类按照某种格式重写。
抽象类和抽象方法的注意事项:
- 抽象类不能实例化
- 抽象类中不一定有抽象方法,有抽象方法一定是抽象类
- 可以有构造方法
- 抽象类的子类:要么重写抽象类的所有抽象方法(更常见);要么是抽象类。
- 抽象类和抽象方法的意义(略)
接口
- 接口的意义(略)
接口不能实例化。
接口和类之间是实现关系,通过implements关键字表示。
接口的子类(实现类):要么重写接口中的所有抽象方法(更常见);要么是抽象类。
注意:
- 接口和类的实现关系,可以单实现,也可以多实现:
1
public class 类名 implements 接口名1, 接口名2 {}
- 实现类还可以在继承一个类的同时实现多个接口:
1
public class 类名 extends 父类 implements 接口名1, 接口名2 {}
接口中成员的特点
- 成员变量:
只能是常量。
默认修饰符:public static final - 没有构造方法。
- 成员方法:
只能是抽象方法。
默认修饰符:public abstract
接口、类的各种关系
- 类和类的关系:继承关系,只能单继承,不能多继承,但是可以多层继承。
- 类和接口的关系:实现关系,可以单实现,也可以多实现,还可以在继承一个类的同时实现多个接口。
- 接口和接口的关系:继承关系,可以单继承,也可以多继承。
细节:如果实现类实现类最下面的子接口,那么就要重写所有的抽象方法。
接口中新增的方法
- JDK7及以前:接口中只能定义抽象方法。
- JDK8新特性:接口中可以定义有方法体的方法。(默认、静态)
- JDK9新特性:接口中可以定义私有方法。
接口中的默认方法(JDK8开始)
- 允许在接口中定义默认方法,需要使用关键字default修饰。
作用:解决接口升级的问题
注意事项:
- 默认方法不是抽象方法,所以不强制被重写。但是如果被重写,重写的时候去掉default关键字。
- public可以省略,default不能省略。
- 如果实现了多个接口,多个接口中存在相同名字的默认方法,子类就必须对该方法进行重写。
接口中的静态方法(JDK8开始)
- 允许在接口中定义静态方法,用static修饰。
注意事项:
- 静态方法只能通过接口名调用,不能通过实现类名或对象名调用。
- public可以省略,static不能省略。
接口中的私有方法(JDK9开始)
分为两种:
- 普通的私有方法(为默认方法服务的)
- 静态的私有方法(为静态方法服务的)
接口的应用
- 接口代表规则,是行为的抽象。想让一个类拥有某个行为,让它实现对应的接口就可以了。
- 当一个方法的参数是接口时,可以传递接口所有实现类的对象,这种方式叫接口多态。
适配器设计模式
解决接口与接口实现类之间的矛盾问题。
- 当一个接口中抽象方法过多,但是我只要使用其中一部分时,就可以使用适配器设计模式。
- 操作过程:写中间类xxxAdapter,实现对应的接口,对接口中的抽象方法进行空实现,再让真正的实现类继承这个中间类,并重写需要用的方法。另外,为了避免其他类创建适配器类的对象,中间的适配器类要用abstact修饰。
- 如果实现类本身需要继承某一个父类,也很好解决——让这个中间类继承那个父类就行。
内部类
- 成员内部类
- 静态内部类
- 局部内部类
- 匿名内部类
内存类的内存图
成员内部类
- 就是写在成员位置的属于外部类的成员。
- 可以被一些修饰符修饰,比如private, 默认, proteted, public, static(静态内部类)等。
- JDK16开始,可以在成员内部类里面定义静态变量,之前不可以。
获取成员内部类对象
- 方式一(当成员内部类被private修饰时):在外部类中编写方法,对外提供内部类对象
- 方式二(当成员内部类被非private修饰时):直接创建对象。
格式:1
Outer.Inner oi = new Outer().new Inner();
- 外部类成员变量和内部类成员变量重名时,在内部类如何访问?
1
System.out.println(Outer.this.变量名);
静态内部类
是一种特殊的成员内部类。
静态内部类只能访问外部类中的静态变量和静态方法,如果想要访问非静态的,需要创建对象。
创建静态内部类对象的格式:
1 | Outer.Inner oi = new Outer().Inner(); |
调用非静态方法的格式:先创建对象,再用对象调用。
调用静态方法的格式:不需要创建对象,直接Outer.Inner.method();
局部内部类
- 将内部类定义在方法中,就叫做局部内部类,类似方法中的局部变量。
- 外界无法直接使用,需要在方法内部创建对象并使用。
- 该类可以直接访问外部类的成员,也可以访问方法内的局部变量。
匿名内部类(重要)
本质上就是隐藏了名字的内部类。它可以写在成员位置,也可以写在局部位置。
格式:
1 | new 类名或接口名() { |
格式的细节:包含了实现关系、方法的重写、创建对象。
从语法的角度看,它整体就是一个类的子类对象或一个接口的实现类对象。
使用场景:当方法的参数是接口或类时。以接口为例,可以传递这个接口的实现类对象。如果实现类只要使用一次,就可以用匿名内部类简化代码。