以前学 Java 确实学得不是很明白,想通过大约 100h 的时间重新学习一编,顺便再学一些之前没有触及过的相对高级一些的知识。
Java 基础——查漏补缺
编码类型
ASCII
介绍
ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是用于电子通信的标准字符编码系统,它包括了128或256个字符,用7位或8位二进制数表示。ASCII编码是计算机中最常用的字符编码方式之一,它被广泛应用于计算机编程和通信中。
特别提示:一个字节可以存储 256 个字符,ASCII 只用了 128 个字符。
缺点
不能表示所有的符号。
应用
如下列举了一些常用的 ASCII 符号:
ASCII编码值 | 字符 | ASCII编码值 | 字符 |
---|---|---|---|
0 | NUL (空字符) | 32 | (空格) |
9 | HT (水平制表符) | 33 | ! (感叹号) |
10 | LF (换行符) | 34 | “ (双引号) |
13 | CR (回车符) | 35 | # (井号) |
31 | 36 | $ (美元符号) | |
32 | (空格) | 37 | % (百分号) |
48 | 0 (数字0) | 57 | 9 (数字9) |
49 | 1 (数字1) | 58 | : (冒号) |
50 | 2 (数字2) | 59 | ; (分号) |
51 | 3 (数字3) | 60 | < (小于号) |
52 | 4 (数字4) | 61 | = (等号) |
53 | 5 (数字5) | 62 | > (大于号) |
54 | 6 (数字6) | 63 | ? (问号) |
55 | 7 (数字7) | 64 | @ (艾特符号) |
56 | 8 (数字8) | 65 | A (大写字母A) |
57 | 9 (数字9) | 66 | B (大写字母B) |
58 | : (冒号) | 67 | C (大写字母C) |
59 | ; (分号) | … | … |
60 | < (小于号) | 97 | a (小写字母a) |
61 | = (等号) | 98 | b (小写字母b) |
62 | > (大于号) | 99 | c (小写字母c) |
63 | ? (问号) | … | … |
64 | @ (艾特符号) | 123 | { (左大括号) |
65 | A (大写字母A) | 124 | |
66 | B (大写字母B) | 125 | } (右大括号) |
67 | C (大写字母C) | 126 | ~ (波浪号) |
… | … | 127 | DEL (删除符) |
Unicode
介绍
Unicode(统一码、万国码、单一码)是一种在计算机中使用的字符编码标准,由国际标准化组织(ISO)制定。Unicode 的目标是统一全球范围内的字符编码,使不同的文字系统,如汉字、假名(平假名和片假名)、西里尔字母、阿拉伯字母等,都能在同一套编码系统中表示。
Unicode 的编码范围覆盖了全球绝大多数的字符,它最初使用 16 位(即 2 字节)来编码,这意味着 Unicode 理论上可以表示 65,536 个不同的字符。然而,随着 Unicode 的发展,为了支持更多的字符,它引入了“辅助平面”(surrogate planes),使得 Unicode 的编码空间扩展到了 32 位。
Unicode 的编码值称为码点(code point),通常写作 U+XXXX
的形式,其中 XXXX
是四位十六进制数。例如,英文字母 “A” 的 Unicode 码点是 U+0041
,由此看出Unicode 码兼容 ASCII 码而汉字 “中” 的 Unicode 码点是 U+4E2D
。
缺点
中英文都用 2 字节保存,浪费空间,因此出现了 UTF-8 等编码。
UTF-8
UTF-8(8位元,Universal Character Set/Unicode Transformation Format)是一种针对Unicode的可变长度字符编码,也称为万国码。UTF-8 用 1 到 6 个字节编码 Unicode 字符,兼容 ASCII 编码,这使得原来处理 ASCII 字符的软件无须或只进行少部分修改后,便可继续使用。
在UTF-8编码中,英文字符使用1个字节存储,重音文字、希腊字母或西里尔字母等使用2个字节存储,常用的汉字使用3个字节存储,而辅助平面字符则使用4个字节存储。这种可变长度的编码方式使得UTF-8在存储和传输文本时更加高效。
基本类型
基本类型所占字节
基本类型 | 占用字节 | 描述 |
---|---|---|
byte |
1 | 8 位有符号二进制整数 |
short |
2 | 16 位有符号二进制整数 |
int |
4 | 32 位有符号二进制整数 |
long |
8 | 64 位有符号二进制整数 |
float |
4 | 32 位 IEEE 754 单精度浮点数 |
double |
8 | 64 位 IEEE 754 双精度浮点数 |
char |
2 | 16 位 Unicode 字符 |
boolean |
不确定 | 通常由 JVM 实现决定,通常占用 1 到多个字节,但在数组中通常占用 4 字节 |
自动类型转换细节
自动转换链:
char -> int -> long -> float -> double
byte -> short -> int -> long -> float ->double
- 有多种类型的数据混合机算时,优先将数据转换为容量最大的类别再计算
- 精度大的类型赋值给精度小的类型会报错,反之会进行自动类型转换
- byte, short 和 char 之间不会相互自动转换
- byte, short, char 三者可以计算,计算时会转为 int 类型
- boolean 不参与转换
- 自动提升原则:表达式结果类型提升为操作中最大的类型
强制类型转换细节
- 进行的数据大小 由大->小时,需要使用到强制类型转换
- 强转符号只能对最近的操作有效,要用小括号提升优先级
1 | int x = (int)10 * 3.5 + 6 * 1.5; // 编译器报错 |
- char 类型可以保存 int 的常量值,但不能保存 int 的变量值,此时需要强转
1 | char c1 = 100; // OK |
- byte 和 short 类型再运算时,当作 int 类型处理
二进制相关知识
原码 反码 补码
概念
原码、反码和补码是计算机中表示有符号整数(通常是二进制数)的三种不同方式。这些编码方式主要用于算术运算,尤其是加法运算,以处理溢出和符号。以下是对这三种编码方式的解释:
- 原码(Sign-Magnitude Representation):
- 原码是最直观的表示法,它直接将符号位(最高位,通常是0表示正数,1表示负数)和数值位(其余位)结合起来。
- 例如,对于8位二进制数,原码表示下的 +5 是
00000101
,而 -5 是10000101
。 - 原码的一个主要问题是加法运算复杂,特别是当两个正数相加结果溢出时,或者一个正数和一个负数相加时。
- 反码(Ones’ Complement):
- 反码是为了简化加法运算而引入的。对于正数,其反码与原码相同;但对于负数,反码是将原码的数值位按位取反(0变为1,1变为0)。
- 例如,对于8位二进制数,+5 的反码是
00000101
,而 -5 的反码是11111010
。 - 反码解决了正负数相加的问题,但仍然不能很好地处理溢出。
- 补码(Two’s Complement):
- 补码是目前计算机中最常用的表示法。对于正数,补码与原码和反码相同;但对于负数,补码是反码加1。
- 例如,对于8位二进制数,+5 的补码是
00000101
,而 -5 的补码是11111011
(反码11111010
加 1)。 - 补码的一个主要优点是它使得加法运算变得简单,因为两个数(无论正负)相加时,只需将它们的补码相加,然后忽略溢出(即忽略最高位的进位)。
- 补码还使得比较运算变得简单,因为所有负数在补码表示下都比任何正数小。
转换
- 二进制的最高位是符号位,0 表示正数,1 表示负数
- 正数的原码,反码,补码都一样
- 负数的反码 = 它的反码符号位不变,其它位取反
- 负数的补码 = 它的反码 + 1 ;负数的反码 = 负数的补码 - 1
- 0 的反码、补码都是 0
- Java 没有无符号数,所有的数都是有符号的
无符号数(Unsigned number)是相对于有符号数而言的,指的是整个机器字长的全部二进制位均表示数值位,相当于数的绝对值。无符号数只用于表示正数,在计算机内部以原码存放,没有符号位。无符号数的表数范围是非负数,其数值范围是从0到最大正整数。例如,对于一个8位无符号整数,其数值范围是0到255。
- 计算机运行时都是以补码运算的
- 计算机结果以原码展示
位运算符
操作符
位运算符 | 描述 | 示例 |
---|---|---|
& (按位与) |
两个相应的二进制位都为1时,结果位才为1。 | 5 & 3 结果为 1 (二进制 0001 ) |
| (按位或) | 两个相应的二进制位中只要有一个为1时,结果位就为1。 | |
^ (按位异或) |
两个相应的二进制位相异时,结果位才为1。 | 5 ^ 3 结果为 6 (二进制 0110 ) |
~ (按位取反) |
反转操作数的所有位,即0变成1,1变成0。 | ~5 结果为 -6 (二进制 ...11110101 ,Java中负数使用补码表示) |
<< (左移) |
把左操作数的所有位向左移动指定的位数,右侧空出的位用0填充。 | 5 << 2 结果为 20 (二进制 10100 ) |
>> (带符号右移) |
把左操作数的所有位向右移动指定的位数,左侧空出的位用符号位填充。 | 5 >> 1 结果为 2 (二进制 00010 ) |
>>> (无符号右移) |
把左操作数的所有位向右移动指定的位数,左侧空出的位总是用0填充,不考虑符号位。 | 5 >>> 1 结果为 2 (二进制 00010 ) |
运算过程
运算过程举例 2&3
- 首先要得到 2 和 3 的原码:2 为 0000 0010 ;3 为 0000 0011
- 然后我们计算 2 和 3 的补码:都是正数,与原码相同
- &: 0000 0010 此时获得的是补码,但是符号位为 0 所以为正数,则原码也是 0000 0010
- 转为十进制 -> 2
运算过程举例 ~-2
- -2 的补码 1000 0010 (原码) -> 1111 1101 (反码) -> 1111 1110 (补码)
- ~-2: 0000 0001 (补码 = 原码)
- 转为十进制 -> 1
运算过程举例 ~2
- 2 的补码 0000 0010
- ~2: 1111 1101 (补码) -> 1111 1100 (-1 得到反码) -> 1000 0011 (非符号位取反 得到原码)
- 转为十进制 -> -3
>>
<<
>>>
的运算本质
>>
相当于除以2,<<
相当于乘以2
举例: -7>>2
- -2 原码 10000000 00000111 -> 反码 11111111 11111000 -> 补码 11111111 11111001
- 带符号右移
>>
: 补码 11111111 11111110 -> 反码 11111111 11111101 -> 原码 10000000 00000010 - 转换为十进制: -2
我们发现如果是正数的话 7 / 2 / 2 = 1,而这里却是 -2 ,所以对于负数来说结果取整的方式不同 (7/4 = 1.725 正数向下取整为 1,-7/4=-1.725 负数向下取整为 -2)
数组
数组的定义方式
1 | double[] hens = {3, 5, 1, 3.4, 2, 50}; |
数组注意事项
- 数组是多个相同类型数据的组合,实现对数据的统一管理
- 数组中的元素可以是任何数据类型,包括基本类型和引用类型,但是不能混用
- 数组创建后如果没有赋值,有默认值:int,short,byte,float,double->0; char->
\u000
; boolean false; String null - 数组属于引用类型,数组型数据是对象 object
数组操作
数组赋值机制
- 基本数据类型赋值,值为具体数据,互不影响
- 数组再默认情况下是引用传递,赋的是地址,这种赋值方式为引用传达
1 | int[] arr = {1, 2, 3}; |
数组拷贝
要使 int[] arr1 = {10, 20, 30}
拷贝到 arr2
数组,要求数据空间是独立的。
1 | int[] arr2 = new int[arr1.length] |
这时就会在堆里面开辟一个新的区域,arr2 指向的也是另一个内存地址。
1 | for(int i = 0; i < arr1.length; i++) { |
这样就能完成拷贝操作。
二维数组
内存布局
1 | int[] arr[][] = new int[2][3]; |
使用细节
- 一维数组声明方式有
int[] x
或int x[]
- 二维数组声明方式有
int[][] y
或int[] y[]
或int y[][]
- 二维数组实际上是由多个一维数组组成的,它的各个一维数组长度可以不相同。
int map[][] = {{1,2}, {3,4,5}};
称为列数不等的二维数组。
算法
冒泡排序
冒泡排序特点
有 n 个元素,进行 n - 1 轮排序,每一轮确定一个最值。进行交换时,如果前面的数大于后面的数就交换。每轮比较的次数都在减少。
实现
1 | public class BubbleSort { |
递归
斐波那契数列
1 | /** |
迷宫问题 (DFS 深度优先搜索)
思路分析:解决 DFS 问题一定要从细节考虑,从整体考虑容易绕晕。带入具体的位置,然后向可以走的方向走,先假设自己的位置是可以走的(先标记为走过),要是往四方走可以走的话就走,如果四方都不能走就标记为死路,注意走过的路不能再走。如果是死路的话就返回 false, 程序就会回溯到上一个 true 的位置。
1 | /** |
八皇后问题
在 8*8 的象棋棋盘上放八个皇后(可以八方攻击)互不攻击,有几种摆法?
思路:第一个皇后放 (1, 1) 然后第二个皇后放 (2, 1) 开始尝试能否被攻击,直到第八个皇后被放下,然后再更换第一个皇后的位置。
// TODO
面向对象程序设计 Object Oriented Programming
对象内存布局
1 | class Cat { |
如果此时我们在用 cat 赋值一个 Cat 类实例 Cat cat2 = cat;
此时 cat2 在栈中会指向堆 0x0011
. 如果此时把 cat.age
改为 10 , 则 cat2.age
也会变为 10.
创建对象的流程:
- 先在方法区加载类信息 (属性与方法信息) ,只会加载一次
- 在堆中分配空间,进行默认初始化 (对象实际上在堆中,栈中存的是对象的引用)
- 完成对象初始化
- 将对象在堆中地址返回给对象的引用
对象的比较
如果有一个所有的属性都和 p 完全一致的 p2 , 那 p == p2
的值是什么?答案为 false。 我们可以通过输出 p 和 p2 的 hashCode()
来比较。
这是因为在比较对象的时候,比较的实际上是内存地址。我们应该使用方法 p.equals()
来比较属性一致的对象。
作用域
变量分为全局变量和局部变量。注意使用细节:
- 属性和局部变量可以重名,访问时遵循就近原则
- 同一个作用域中两个局部变量不能重名
- 属性的生命周期较长,伴随对象创建而创建,伴随对象销毁而销毁。局部变量生命周期较短,伴随代码块执行而创建,伴随代码块结束而销毁,即生命周期为一次方法调用。
- 作用域范围不同:
- 全局变量(属性):可以被本类或其他类(通过对象调用)使用
- 局部变量:只能在本类中对应的方法中使用
- 修饰符不同:
- 全局变量(属性)可以加修饰符
- 局部变量不能加修饰符
this 对象
Java 虚拟机会给每个对象分配一个 this 代表当前对象。
我们可以通过输出变量的 hashCode 来大致认为地址值。
this 的使用细节:
- this 关键字可以用来访问本类的属性、方法、构造器
- this 用于区分当前类的属性和局部变量
- 访问成员方法的语法:`this.方法名(参数列表)``
- 访问构造器语法:
this(参数列表);
注意!只能在构造器中使用!即只能在一个构造器访问另外一个构造器 - this 不能在类定义的外部使用,只能在类定义的方法中使用
方法机制
方法调用机制
1 | class Person { |
- 当程序执行到方法时,就会开辟一个独立的栈空间
- 方法执行完毕或 return 时,就会返回
- 返回到调用方法的地方
- 继续执行后面的代码
方法传参机制
1 | public class Main { |
在这段代码中为什么 main() 中 a 和 b 没有被交换呢?不同的栈是独立的空间。
在 main栈 中,定义了 a 和 b , 运行到 obj.swap(); 时前往 swap栈,此时 swap 中参数 a=10, b=20; swap栈 中的 a 和 b 交换了,但是 main栈中的 a 和 b 没有被交换。
那如果是数组呢? 假设主方法中有一个数组 int arr[] = {1, 2, 3};
, 在类 B 中定义一个公共的修改数组第一个元素为200的函数 public void test(int[] arr){arr[0] = 200;}
,在主函数中创建 B 的实例 b: B b = new B();
, 在主方法中运行 b.test(arr)
, 此时发现主方法中的 arr[0]
变为了 200.
这是因为,将 main栈 中 arr 传入 test 栈时,传递的是内存地址,而 arr 中修改时也是通过内存地址来在堆中修改数据,所以修改了堆中地址对应的值,在重新引用主方法中的 arr[0]
时就被修改成了 200.
所以在处理参数的时候,我们要注意传递的是地址还是值。
1 | public class Main { |
在上面的例子中,执行 b.test200(p)
时,是在 test200栈 中把 p 变为 null, 就失去了和堆的链接,所以对主方法的 p 不影响,main栈 中的 p 还是指向没有修改过的堆。
方法重载 (OverLoad)
Java 中允许同一个类中,多个同名方法的存在,但要求形参列表不一致。例如 System.out.println();
其中 out 是 PrintStream 类型。
重载的好处:
- 减轻了起名的麻烦
- 减轻了记名的麻烦
重载注意事项:
- 方法名必须一样
- 形参列表必须不一样
- 返回类型无要求(但是如果形参相同,那返回类型不同不构成方法重载)
可变参数
Java 允许同一个类中同名同功能但参数个数不同的方法封装成一个方法。使用
1 | 访问修饰符 返回类型 方法名 (数据类型... 方法名) |
来定义可变参数。例如:
1 | private int sum(int... nums) { |
使用注意细节:
- 可变参数实参可以为任意个数
- 可变参数实参可以为数组
- 可变参数实参本质是数组
- 可变参数可以和普通类型一起放在形参列表,但是只能放在最后
- 一个形参列表中只能出现一个可变形参
构造方法
构造器说明:
- 修饰符可以是默认,也可以是 public protected private
- 构造器没有返回值
- 方法名和类名必须一致
- 参数列表与成员方法规则一致
- 构造器调用由系统完成
- 构造器也可以重载,方法和方法重载相同
- 如果没有指定构造器,系统默认生成无参构造器(默认构造器)可以用
javap
命令反编译查看 - 一旦定义了构造器,默认构造器被覆盖,不能再使用,除非显式定义
包
包的基本语法:package 包名
包的三大作用:
- 区分相同名字的类
- 类多时方便管理类
- 控制访问范围
访问修饰符
Java 提供四种访问控制修饰符号,用于控制方法和属性的访问权限
- 公开级别:
public
对外公开 - 受保护级别:
protected
对子类和同一个包中的类公开 - 默认级别:没有修饰符号,向同一个包中的类公开
- 私有级别:
private
只有类本身可以访问,不对外公开
访问修饰符 | 当前类 | 同一包中的其他类 | 子类 | 其他包中的类 |
---|---|---|---|---|
private | 是 | 否 | 否 | 否 |
default (无修饰符) | 是 | 是 | 否 | 否 |
protected | 是 | 是 | 是 | 否 |
public | 是 | 是 | 是 | 是 |
面向对象三大特征
封装 (encapsulation)
封装是把抽象出的数据(属性)和对数据的操作(方法)封装在一起,数据被保护在内部,程序的其它部分只有通过被授权的操作(方法)才能对数据进行操作。
使用封装可以隐藏实现细节,对数据进行验证,保证安全合理。
封装的实现步骤:
- 先对属性进行私有化 private , 使外界不能直接修改属性
- 提供 public 的 set 方法,用于对属性判断并赋值
1 | public void setXxx(类型 参数名) { |
- 提供 public 的 get 方法,用于获得属性值
1 | public 类型 getXxx() { |
这时如果通过构造方法,那设置的封装就无效了,此时我们在构造方法中调用 setter 来保证封装。
继承
继承可以解决代码复用,让编程更加靠近人类思维,当多个类存在相同的属性时可以在类中抽象出父类,在父类中定义相同的属性和方法,所有的子类不需要重新定义这些方法和属性,只需要通过 extends
来表明继承父类。
1 | class 子类 extends 父类 { |
子类会自动拥有(非私有)父类定义的属性和方法,父类又叫超类,基类;子类又叫派生类。
继承使用细节:
- 子类继承了所有的属性和方法,但是私有属性和方法不能在子类直接访问,要通过公共方法访问
- 子类必须调用父类的构造器,完成父类的初始化
- 创造子类对象时,不管使用子类哪个构造器,默认情况总会调用父类的无参构造器,如果父类没有无参构造器,则必须在子类构造器中用
super
指定使用父类构造器完成对父类的初始化工作 - 如果希望指定调用父类构造器,则显式调用
super(参数列表);
- super 在使用时需要放在构造器第一行
super();
和this();
都要放在第一行,因此两个方法不能共存在一个构造器中- Java 中所有类都是 Object类 的子类,Object 是所有类的父类
- 父类构造器的调用不限于直接父类,将向上追溯直到 Object 类
- Java 是单继承机制,子类最多只能继承一个父类
- 不能滥用继承,子类和父类见必须满足 is-a 的逻辑关系
1 | class GrandPa { |
此时在 main栈 中运行 Son son = new Son();
内存中发生了什么?
son 访问属性时的流程:先查看子类是否有该属性 -> 若没有就向上父类询问
如果 Father 中的 age 为 private 而 GrandPa 中的 age 为 public 也不能访问到,因为追溯到 Father 的 age 发现为 private 时就直接报错了。
super
super 代表父类的引用,用于访问父类的属性、方法、构造器。 super.方法名
访问父类非私有属性; super.方法名(参数列表);
访问父类非私有方法; super(参数列表);
放在构造器第一句,只能出现一句。
使用 super 的细节:
- 调用父类构造器的好处:分工明确(父类属性由父类初始化,子类属性由子类初始化)
- 若子类和父类由重名的成员,为了访问父类的成员必须通过 super ,如果没有重名,this、super、直接访问效果一致
- super 的访问不限于直接父类,如果 GrandPa 和本类有重名成员,也可以通过 super 访问 GrandPa 成员。如果多个父类中都有同名成员,使用 super 遵循就近原则
区别点 | this | super |
---|---|---|
访问属性 | 访问本类中的属性,如果本类没有从父类查找 | 访问父类属性 |
调用方法 | 访问本类中的方法,如果没有从父类查找 | 直接访问父类方法 |
调用构造器 | 调用本类构造器,必须放在首行 | 调用父类构造器,必须放在首行 |
特殊 | 表示当前对象 | 子类中访问父类对象 |
方法重写/覆盖 (override)
方法重写细节:
- 子类方法的参数,方法名称,要和父类完全一致
- 子类方法的返回类型和父类方法的返回类型一样,或是返回父类返回类型的子类,例如父类返回 Object,子类返回 String
- 子类方法不能缩小父类的访问权限
方法重写和方法重载的区别
名称 | 发生范围 | 方法名 | 参数列表 | 返回类型 | 修饰符 |
---|---|---|---|---|---|
重载 overload | 本类 | 必须一样 | 类型,个数或顺序至少有一个不同 | 无要求 | 无要求 |
重写 override | 父子类 | 必须一样 | 相同 | 子类重写的方法和父类返回的类型一致,或是父类返回类型的子类 | 子类方法不能缩小父类的访问范围 |
多态 (polymorhic)
通过一个例子引出多态:定义一个 feed 方法,因为要处理不同的类型的 Animal 子类,就要写很多个功能一致的函数……
方法和对象具有多种形态,是面向对象的第三特征,多态是建立在继承和封装之上的。
方法的多态
重写和重载体现多态。
对象的多态 (多态的核心)
重要的几句话:
- 一个对象的编译类型和运行类型可以不一致
- 编译类型在定义对象时就确定了,不能改变
- 运行类型可以是变化的
- 编译类型看定义时
=
的左边,运行类型看=
的右边
例如对于以下代码
1 | Animal animal = new Dog(); // 编译类型时 Animal,运行类型是 Dog |
向上转型
- 本质:父类的引用指向了子类的对象
- 语法:
父类 name = new 子类();
- 特点:编译类型看左边,运行类型看右边,可以依权限调用父类成员,不能调用子类特有成员(因为在编译阶段能调用的成员由编译类型决定),运行效果看子类
1 | Animal animal = new Cat(); // 父类引用指向子类对象 |
如果访问的内容不在方法区内,就会指向编译类型的堆:
1 | // class Base -> int count = 10; |
向下转型
- 语法:
子类类型 name = (子类类型)父类引用;
- 只能强转父类的引用,不能强转父类的对象
- 要求父类的引用必须指向的是当前目标类型的对象
- 向下转型后,可以调用子类类型中的成员
1 | Animal animal = new Cat(); // animal 指向一个 Cat 对象 |
动态绑定机制
- 调用方法时,方法会和内存地址/运行类型绑定
- 调用对象属性时,没有动态绑定机制,哪里运行哪里使用
1 | class A { |
可以通过关键字 instanceof 来判断运行类型。
Object 类
Object
类是 Java 中所有类的超类。
方法名 | 描述 |
---|---|
public final Class<?> getClass() |
返回此对象运行时类的 Class 对象。 |
public int hashCode() |
返回此对象的哈希码值。 |
public boolean equals(Object obj) |
将此对象与指定的对象进行比较以确定它们是否相等。 |
protected Object clone() |
创建并返回此对象的一个副本。 |
public String toString() |
返回此对象的字符串表示形式。 |
public final void notify() |
唤醒在此对象监视器上等待的单个线程。 |
public final void notifyAll() |
唤醒在此对象监视器上等待的所有线程。 |
public final void wait(long timeout) throws InterruptedException |
使当前线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者经过指定的时间量。 |
public final void wait(long timeout, int nanos) throws InterruptedException |
使当前线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他线程中断当前线程,或者经过指定的时间量(以纳秒为单位)。 |
public final void wait() throws InterruptedException |
使当前线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法。 |
protected void finalize() throws Throwable |
当垃圾收集器确定不存在对该对象的更多引用时,由对象的垃圾收集器调用此方法。 |
这些方法为 Java 对象提供了基本的行为和比较机制,同时也支持多线程编程中的线程同步。需要注意的是,虽然这些方法在 Object
类中定义,但子类可以重写其中的一些方法(例如 equals()
, hashCode()
, toString()
, clone()
, 和 finalize()
)以提供特定的行为。
==
用于比较时,如果是基本类型,就会判断值是否相等;如果是引用类型,就会判断地址是否相等。
我们可以通过重写 Object 中的方法来完成特定功能。
finalize()
- 对象被回收时,系统自动调用 finalize 方法。子类可以重写该方法用于释放资源
- 当某个对象没有任何引用时,JVM 认为这个对象是一个垃圾对象,使用垃圾回收机制销毁对象,销毁前调用 finalize()
- 垃圾回收机制的调用是由系统决定的,也可以通过 System.gc() 主动触发垃圾回收机制‘
1 | Car bwm = new Car("宝马"); |
此时 Car 在堆中的对象没有任何引用,就会被系统回收。
从Java9开始,finalize方法已被标注为@Deprecated,也就是过期了!
类变量 (静态变量) 和类方法
是该类所有对象共享的变量,任何实例取到的都是同一个值,修改时也修改的同一个变量。
1 | 访问修饰符 static 数据类型 变量名; [推荐] |
1 | public class Child { |
count 在内存中的位置是哪里?有不同的说法
- 在方法区中有一个空间叫静态域,count 在静态域中
- count 直接存放在堆中
实际上,要根据 JDK 版本确定,在 JDK 7 以前放在方法区中,在之后放在堆中。
类变量使用细节:
- 需要让某个类的所有对象共享一个变量时可以用类变量,
- 类变量是该类所有实例共享的,实例变量是每个对象独享的
- 类变量可以通过
类名.类变量名
或对象名.类变量名
来访问,但是推荐使用类名.类变量名
。
何时使用类方法?当方法中不涉及到任何和对象相关的成员,就可以将方法设计为静态方法,提高开发效率。常用于工具类中,可以直接调用方法。
1 | 访问修饰符 static 返回类型 方法名() {} [推荐] |
调用方法和类变量一致。
理解 main() 方法
- main() 方法由 Java 虚拟机调用
- Java 虚拟机需要调用 main() 方法,所以访问权限必须是 public
- Java 虚拟机在执行 main() 方法时不必创建对象,所以必须是 static
- main() 方法接受 String 类型的数组参数,该数组中保存执行 Java 命令传递的类参数
java 执行的程序 参数1 参数...
代码块(初始化块)
属于类中的成员,类似于方法,将逻辑语句封装在方法体中,通过 {}
包围。和方法不同,没有方法名,没有返回,没有参数,只有方法体,而且用显式调用,在加载类或创建对象时调用。
1 | (static) { |
没有 static 修饰的成为普通代码块/非静态代码块,通过 static 修饰的为静态代码块。
代码块相当于另一种形式的构造器,可以做初始化操作。如果多个构造器中都有重复语句,可以抽取到初始化块中,提升代码重用性。
代码块的使用细节:
- 代码块的作用是对类初始化,伴随着类的加载而执行,只执行一次。如果是非静态代码块,在每次创建对象都会执行。
- 类什么时候被加载
- 创建对象实例时 (new)
- 创建子类对象实例,父类也会被加载
- 使用类的静态成员时(静态属性,静态方法)
- 普通的代码块在创建对象时会被隐式调用,被创建一次就会调用一次,但是使用静态成员时普通代码块不会执行。
- 创建对象时,调用顺序
- 调用静态代码块和静态属性初始化 (优先级一致,按照定义的顺序调用)
- 调用普通代码块和普通属性的初始化 (优先级一致,按照定义的顺序调用)
- 调用构造方法
- 构造方法的最前面隐藏了
super();
和调用普通代码块,而静态代码块和属性是在类加载时执行,所以优先于非静态 - 创建一个子类对象时,调用顺序如下
- 父类的静态代码块和静态属性初始化(优先级一致,按照定义的顺序调用)
- 子类的静态代码块和静态属性初始化(优先级一致,按照定义的顺序调用)
- 父类的普通代码块和普通属性初始化(优先级一致,按照定义的顺序调用)
- 父类的构造方法
- 子类的普通代码块和普通属性初始化(优先级一致,按照定义的顺序调用)
- 子类的构造方法
- 静态代码块只能直接调用静态成员,普通代码块可以调用任意成员。
final 关键字
使用场景:
- 不希望类被继承可以使用 final
- 不希望父类的某个方法被重写/覆盖,可以用 final
- 不希望类的某个属性被修改,可以用 final
- 不希望局部变量被修改,可以用 final
注意事项:
- final 修饰的属性又叫做 常量
- final 修饰的属性在定义时必须赋初值,并且不能再修改,可以在一下位置赋值
- 定义时
- 构造器中
- 代码块中
- final 修饰的属性时静态的话,初始化的位置只能是:
- 定义时
- 静态代码块中 (不能再构造器中赋值)
- final 类不能继承,但是可以实例化对象
- 类不是 final 类,但是又 final 方法,方法不能重写,但是类可以继承
- 如果类已经是 final 类,没有必要修饰方法为 final
- fina 和 static 通常搭配使用,编译器底层做了优化处理
- 包装类(Integer,Double,Float,Boolean)都是 final 类,String 也是 final 类
abstract 关键字
- 抽象类用 abstract 修饰类
- 用 abstract 修饰方法,就是抽象方法(抽象方法没有方法体)
- 抽象类的价值在于设计,设计好后子类继承实现
- 抽象类再框架和设计模式中使用较多
- 抽象类可以有任意成员(抽象类是类),可以拥有非抽象方法,构造器,静态属性等
- 抽象方法不能有主题,即不能实现方法
接口 - interface
1 | [修饰词] interface [name] { |
在 JDK7 前,接口中的所有方法都没有方法体,即都是抽象方法,JDK8 后接口类可以有静态方法,默认方法,也就是说接口中可以有具体的方法实现。
注意事项
- 接口不能被实例化
- 接口中所有方法都是 public 方法,接口中的抽象方法可以不用 abstract 修饰
- 普通类实现接口必须将接口所有方法实现
- 抽象类实现接口可以不实现方法
- 一个类可以实现多个接口
implements IA, IB
- 接口中的属性是
public static final
修饰的 - 接口属性访问形式和静态属性访问形式相同
- 接口不能继承其它类,但是可以继承多个别的接口
- 接口的修饰符只能是 public 和 默认,与类的修饰符一致
接口与继承
继承的价值在于解决代码的复用性和可维护性,接口主要价值在于设计规范。
继承是 is-a 的关系,接口是 like-a 的关系。
接口在一定程度上实现代码解耦,即接口规范性和动态绑定。
接口的多态
- 接口方法中的参数类型多态,如 Usb 实例既可以接收手机对象,又可以接受相机对象
- 多态数组:在 Usb 数组中存放 Phone,Camera 对象
- 接口存在多态传递现象——即接口的接口子类被实现后,接口和接口子类的方法都需要被实现
内部类
一个类的内部又完整的嵌套了另一个类结构,被嵌套的类称为内部类 (inner class), 嵌套其它类的类称为外部类 (outer class)。这是类的五大成员之一 (属性,方法,构造器,代码块,内部类) 。内部类的最大特点就是可以直接访问私有属性,并且可以体现类与类之间的包含关系。
1 | class Outer { |
局部内部类
说明:局部内部类是定义在外部类的局部位置,比如方法中,而且有类名
- 可以直接访问外部类的所有成员,包括是非私有的
- 不能直接添加访问修饰符,因为它的地位就是局部变量,局部变量不能使用修饰符,但是可以用 final 修饰
- 作用域:仅仅在定义它的方法和代码块中
- 局部内部类访问外部类成员:直接访问
- 外部类访问内部类成员:创建对象再访问
- 外部其它类不能访问局部内部类,因为它相当于局部变量
- 如果外部类和局部内部类成员重名,遵循就近原则,要访问外部类成员可以使用
外部类名.this.成员
。本质:外部类.this
就是外部类对象,谁调用了它,this 就指向谁
1 | class Outer02 { |
匿名内部类
注意事项:
- 基本语法:
new 类或接口(参数列表){ ... };
- 匿名内部类既是一个类的定义,同时本身也是一个对象,从语法上看既有定义类的特征也有创建对象的特征,可以调用匿名内部类的方法:
1 | new A() { |
- 可以直接访问外部类所有成员,包括私有的
- 不能添加访问修饰符,因为地位是局部变量
- 作用域:在定义它的方法或代码块中
- 访问外部成员:直接访问
- 外部其它类:不能访问
- 外部类和内部类成员重名使用 this 关键字
成员内部类
成员内部类定义在外部类的成员位置,并且没有static修饰:
- 可以直接访问外部类的所有成员,包括私有的
- 可以添加任意修饰符,地位相当于成员
- 作用域:与成员一样为整个整体。可以在外部类的成员方法中创建内部类对象再调用方法获取
- 成员内部类访问外部类:直接访问
- 外部类访问成员内部类:创建对象再访问
- 外部其它类访问成员内部类
- 重名使用 this
静态内部类
成员内部类定义在外部类的成员位置,有static修饰:
- 可以直接访问所有的静态成员
- 可以添加任意修饰符,地位相当于成员
- 作用域:与成员一样为整个整体
- 静态内部类访问外部类:直接访问静态成员
- 外部类访问静态内部类:创建对象后访问
- 重名使用 this
枚举类 (enumeration)
枚举是一组常量的集合,属于一种特殊的类,包含一组有限的特殊对象。
我们可以自定义一个枚举类,只需要让对象静态公开且最终,构造方法私有化来实现。
1 | public enum Season { |
- 当我们使用 enum 关键字来开发一个枚举类时,默认会继承 Enum 类,可以通过 javap 命令反编译证明
- 传统的
public static final Season SPRING = new Season("春天", "温暖);
简化为SPRING("春天", "温暖")
,这里必须知道它调用的是哪个构造器 - 如果使用无参构造器创建枚举对象,则实参列表和小括号都可以省略
- 当有多个枚举对象时使用
,
分隔,最有一个;
结尾 - 枚举对象必须放在枚举类的行首
枚举类的成员方法
toString
: Enum 类中重写过了,返回当前的对象名,子类可以重写该方法name
: 返回对象名,子类不能重写ordinal
: 返回对象的位置号,默认从 0 开始values
: 返回枚举类中的所有常量valueOf
: 将字符串转换为枚举对象,要求字符串必须为常量名compareTo
: 比较枚举常量 (比较的就是位置号)
使用注意事项:
- 使用了 enum 关键字后,就不能再继承其它类,因为 enum 会 隐式继承 Enum,而 Java 是单继承机制
- 枚举类和普通类一样可以实现接口
基本注解
注解 (Annotation) 也被称为元数据 (Metadata) , 用于修饰解释 包、类、方法、构造器、属性、局部变量等数据信息。和注释一样注解不影响程序逻辑,但是注解可以被编译或运行,相当于嵌入在代码中的补充信息。在 JavaSE 中,注解的使用目的比较简单,往往是用于标记过时的功能,忽略警告等。在 JavaEE 中注解占据了更加重要的角色,例如用来配置应用程序的切面,代替 JavaEE 旧版中遗留的繁冗代码和 XML 配置等。
@Override
: 限定方法为重写父类方法,只能用于方法@Deprecated
: 用于表示某个程序元素已过时SuppressWarning
: 抑制编译器警告
@Override
我们查看 Override 的原码,发现为:
1 |
|
其中 @interface
表示注解而非接口,@Target(ElementType.METHOD)
说明只能修饰方法,@Target
是修饰注解的注解,成为元注解。
@Deprecated
修饰某个元素,表示该元素已经过时。表示的是不推荐使用,但是仍然可以使用。观察原码发现使用 @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE})
修饰,即这些都可以被 @Deprecated 注解。
@SuppressWarning
@SuppressWarnings
注解有一个 value
参数,用于指定要忽略的警告类型。常见的警告类型包括:
all
:忽略所有类型的警告。unchecked
:忽略未经检查的警告,通常在使用泛型时出现。deprecation
:忽略使用已过时的API的警告。rawtypes
:忽略使用不带泛型类型的原始类型的警告。unused
:忽略未使用的代码或变量的警告。restriction
:忽略使用了受限制的API的警告,通常用于访问非公开或不稳定的API。
元注解
JDK 的元 Annotation 用于修饰其它的 Annotation。
@Retention
只能修饰一个 Annotation 定义,用于指定该 Annotation 可以保留多长时间,@Retention
必须包含一个 RetentionPolicy 类型的成员变量,使用时用 value 成员指定值,有RetentionPolicy 中的 SOURCE, CLASS, RUNTIME。例如@Override
中的@Retention
为SOURCE
,也就是 class 文件和 JVM 中都不会保留该注解@Target
用于指定 Annotation 可以修饰哪些元素。@Documented
用于指定被该元 Annotation 修饰的 Annotation 类将被 javadoc 工具提取成文档,即生成文档时可以看到该注解。定义为 Documented 的注解必须设置 Retention 值为 RUNTIME@Inherited
被它修饰的 Annotation 具有继承性,即子类会自动具有该 Annotation