MxHanks' Blog

奔赴山海,保持热爱

0%

Java基础-基本类型,二进制,数组,基础算法,面向对象

以前学 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

  1. 有多种类型的数据混合机算时,优先将数据转换为容量最大的类别再计算
  2. 精度大的类型赋值给精度小的类型会报错,反之会进行自动类型转换
  3. byte, short 和 char 之间不会相互自动转换
  4. byte, short, char 三者可以计算,计算时会转为 int 类型
  5. boolean 不参与转换
  6. 自动提升原则:表达式结果类型提升为操作中最大的类型

强制类型转换细节

  1. 进行的数据大小 由大->小时,需要使用到强制类型转换
  2. 强转符号只能对最近的操作有效,要用小括号提升优先级
1
2
int x = (int)10 * 3.5 + 6 * 1.5; // 编译器报错
int x = (int)(10 * 3.5 + 6 * 1.5); // 正确写法
  1. char 类型可以保存 int 的常量值,但不能保存 int 的变量值,此时需要强转
1
2
3
4
char c1 = 100; // OK
int m = 100;
char c2 = m; // 报错
char c2 = (char)m; // OK
  1. byte 和 short 类型再运算时,当作 int 类型处理

二进制相关知识

原码 反码 补码

概念

原码、反码和补码是计算机中表示有符号整数(通常是二进制数)的三种不同方式。这些编码方式主要用于算术运算,尤其是加法运算,以处理溢出和符号。以下是对这三种编码方式的解释:

  1. 原码(Sign-Magnitude Representation)
    • 原码是最直观的表示法,它直接将符号位(最高位,通常是0表示正数,1表示负数)和数值位(其余位)结合起来。
    • 例如,对于8位二进制数,原码表示下的 +5 是 00000101,而 -5 是 10000101
    • 原码的一个主要问题是加法运算复杂,特别是当两个正数相加结果溢出时,或者一个正数和一个负数相加时。
  2. 反码(Ones’ Complement)
    • 反码是为了简化加法运算而引入的。对于正数,其反码与原码相同;但对于负数,反码是将原码的数值位按位取反(0变为1,1变为0)。
    • 例如,对于8位二进制数,+5 的反码是 00000101,而 -5 的反码是 11111010
    • 反码解决了正负数相加的问题,但仍然不能很好地处理溢出。
  3. 补码(Two’s Complement)
    • 补码是目前计算机中最常用的表示法。对于正数,补码与原码和反码相同;但对于负数,补码是反码加1。
    • 例如,对于8位二进制数,+5 的补码是 00000101,而 -5 的补码是 11111011(反码 11111010 加 1)。
    • 补码的一个主要优点是它使得加法运算变得简单,因为两个数(无论正负)相加时,只需将它们的补码相加,然后忽略溢出(即忽略最高位的进位)。
    • 补码还使得比较运算变得简单,因为所有负数在补码表示下都比任何正数小。

转换

  1. 二进制的最高位是符号位,0 表示正数,1 表示负数
  2. 正数的原码,反码,补码都一样
  3. 负数的反码 = 它的反码符号位不变,其它位取反
  4. 负数的补码 = 它的反码 + 1 ;负数的反码 = 负数的补码 - 1
  5. 0 的反码、补码都是 0
  6. Java 没有无符号数,所有的数都是有符号的

    无符号数(Unsigned number)是相对于有符号数而言的,指的是整个机器字长的全部二进制位均表示数值位,相当于数的绝对值。无符号数只用于表示正数,在计算机内部以原码存放,没有符号位。无符号数的表数范围是非负数,其数值范围是从0到最大正整数。例如,对于一个8位无符号整数,其数值范围是0到255。

  7. 计算机运行时都是以补码运算
  8. 计算机结果以原码展示

位运算符

操作符

位运算符 描述 示例
& (按位与) 两个相应的二进制位都为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
  1. 首先要得到 2 和 3 的原码:2 为 0000 0010 ;3 为 0000 0011
  2. 然后我们计算 2 和 3 的补码:都是正数,与原码相同
  3. &: 0000 0010 此时获得的是补码,但是符号位为 0 所以为正数,则原码也是 0000 0010
  4. 转为十进制 -> 2
运算过程举例 ~-2
  1. -2 的补码 1000 0010 (原码) -> 1111 1101 (反码) -> 1111 1110 (补码)
  2. ~-2: 0000 0001 (补码 = 原码)
  3. 转为十进制 -> 1
运算过程举例 ~2
  1. 2 的补码 0000 0010
  2. ~2: 1111 1101 (补码) -> 1111 1100 (-1 得到反码) -> 1000 0011 (非符号位取反 得到原码)
  3. 转为十进制 -> -3
>> << >>> 的运算本质

>> 相当于除以2,<< 相当于乘以2

举例: -7>>2

  1. -2 原码 10000000 00000111 -> 反码 11111111 11111000 -> 补码 11111111 11111001
  2. 带符号右移 >>: 补码 11111111 11111110 -> 反码 11111111 11111101 -> 原码 10000000 00000010
  3. 转换为十进制: -2

我们发现如果是正数的话 7 / 2 / 2 = 1,而这里却是 -2 ,所以对于负数来说结果取整的方式不同 (7/4 = 1.725 正数向下取整为 1,-7/4=-1.725 负数向下取整为 -2)

数组

数组的定义方式

1
2
double[] hens = {3, 5, 1, 3.4, 2, 50};
double[] lateDefineArr = new double[10];

数组注意事项

  1. 数组是多个相同类型数据的组合,实现对数据的统一管理
  2. 数组中的元素可以是任何数据类型,包括基本类型和引用类型,但是不能混用
  3. 数组创建后如果没有赋值,有默认值:int,short,byte,float,double->0; char->\u000; boolean false; String null
  4. 数组属于引用类型,数组型数据是对象 object

数组操作

数组赋值机制

  1. 基本数据类型赋值,值为具体数据,互不影响
  2. 数组再默认情况下是引用传递,赋的是地址,这种赋值方式为引用传达
1
2
3
int[] arr = {1, 2, 3};
int[] arr2 = arr1;
arr[0] = 10

png

数组拷贝

要使 int[] arr1 = {10, 20, 30} 拷贝到 arr2 数组,要求数据空间是独立的。

1
int[] arr2 = new int[arr1.length]

这时就会在堆里面开辟一个新的区域,arr2 指向的也是另一个内存地址。

1
2
3
for(int i = 0; i < arr1.length; i++) {
arr1[i] = arr1[i]
}

这样就能完成拷贝操作。

二维数组

内存布局

1
2
int[] arr[][] = new int[2][3];
arr[1][1] = 8;

png

使用细节

  1. 一维数组声明方式有 int[] xint x[]
  2. 二维数组声明方式有 int[][] yint[] y[]int y[][]
  3. 二维数组实际上是由多个一维数组组成的,它的各个一维数组长度可以不相同。int map[][] = {{1,2}, {3,4,5}}; 称为列数不等的二维数组。

算法

冒泡排序

冒泡排序特点

有 n 个元素,进行 n - 1 轮排序,每一轮确定一个最值。进行交换时,如果前面的数大于后面的数就交换。每轮比较的次数都在减少。

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class BubbleSort {

public static void main(String[] agrs) {

int[] arr = {24, 69, 80, 57, 13, 12, 438, 1, -1, 283};
int temp;

// 外层循环为 n-1 轮
for(int i = 0; i < arr.length - 1; i++) {
// 4次 3次 2次 1次
for (int j = 0; j < arr.length - i - 1; j++) {
if(arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
System.out.println("===第" + (i+1) + "轮===");
for (int j = 0; j < arr.length; j++) {
System.out.print(arr[j] + ", ");
}
System.out.println();
}

}

}

递归

斐波那契数列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 实行斐波那契数列,给出一个数 n 判断数列第 n 个的值
* 规定第一个,第二个为 1 , 其余为前两个之和
* 1 1 2 3 5 8 13 21...
*/
public class Fibonacci {
public static void main(String[] args) {
T t = new T();
System.out.println(t.fibonacci(7));
}
}

class T {
public int fibonacci(int index) {
if(index == 1 || index == 2) {
return 1;
} else {
return fibonacci(index - 1) + fibonacci(index - 2);
}
}
}

迷宫问题 (DFS 深度优先搜索)

思路分析:解决 DFS 问题一定要从细节考虑,从整体考虑容易绕晕。带入具体的位置,然后向可以走的方向走,先假设自己的位置是可以走的(先标记为走过),要是往四方走可以走的话就走,如果四方都不能走就标记为死路,注意走过的路不能再走。如果是死路的话就返回 false, 程序就会回溯到上一个 true 的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
* 要从指定位置
* 走到 右下角的 0
*/
public class PuzzelWay {
public static void main(String[] args) {
int[][] map = {
{1, 1, 1, 1, 1, 1, 1},
{1, 0, 0, 0, 1, 0, 1},
{1, 0, 1, 0, 0, 0, 1},
{1, 1, 1, 1, 0, 0, 1},
{1, 0, 0, 0, 0, 0, 1},
{1, 0, 1, 0, 0, 1, 1},
{1, 0, 1, 0, 0, 0, 1},
{1, 1, 1, 1, 1, 1, 1}
};
PuzzelWay pzw = new PuzzelWay();
for(int i = 0; i < map.length; i++) {
for(int j = 0; j < map[i].length; j++) {
System.out.print(map[i][j] + " ");
}
System.out.println();
}
System.out.println();
pzw.findWay(map, 1, 1);
for(int i = 0; i < map.length; i++) {
for(int j = 0; j < map[i].length; j++) {
System.out.print(map[i][j] + " ");
}
System.out.println();
}
}
/**
* map 表示迷宫
* i, j 表示位于 map[i][j]
* 0 表示可以走
* 1 表示障碍
* 2 表示可以走
* 3 表示走过是死路
*/
private boolean findWay(int[][] map, int i, int j) {
if(map[6][5] == 2) {
return true;
} else {
// 表示这里可以走
if(map[i][j] == 0) {
// 假设可以走通
map[i][j] = 2;
if (findWay(map, i+1, j)) { // 向下走
return true;
} else if (findWay(map, i, j+1)) { // 向右
return true;
} else if (findWay(map, i-1, j)) { // 向上
return true;
} else if (findWay(map, i, j-1)) { // 向下
return true;
} else {
map[i][j] = 3;
return false;
}
} else {
return false;
}
}
}
}

八皇后问题

在 8*8 的象棋棋盘上放八个皇后(可以八方攻击)互不攻击,有几种摆法?

思路:第一个皇后放 (1, 1) 然后第二个皇后放 (2, 1) 开始尝试能否被攻击,直到第八个皇后被放下,然后再更换第一个皇后的位置。

// TODO

面向对象程序设计 Object Oriented Programming

对象内存布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Cat {
// 属性 = 成员变量 = field(字段)
String name;
int age;
String color;
// 对象的属性默认值遵守数组规则
}

...

Cat cat = new Cat();
cat.name = "小白";
cat.color = "白色"
cat.age = 12;

Cat cat2 = cat1;
cat1.age = 10;
print(cat2.age);

如果此时我们在用 cat 赋值一个 Cat 类实例 Cat cat2 = cat; 此时 cat2 在栈中会指向堆 0x0011. 如果此时把 cat.age 改为 10 , 则 cat2.age 也会变为 10.

创建对象的流程:

  1. 先在方法区加载类信息 (属性与方法信息) ,只会加载一次
  2. 中分配空间,进行默认初始化 (对象实际上在堆中,栈中存的是对象的引用)
  3. 完成对象初始化
  4. 将对象在堆中地址返回给对象的引用

对象的比较

如果有一个所有的属性都和 p 完全一致的 p2 , 那 p == p2 的值是什么?答案为 false。 我们可以通过输出 p 和 p2 的 hashCode() 来比较。

这是因为在比较对象的时候,比较的实际上是内存地址。我们应该使用方法 p.equals() 来比较属性一致的对象。

作用域

变量分为全局变量和局部变量。注意使用细节:

  1. 属性和局部变量可以重名,访问时遵循就近原则
  2. 同一个作用域中两个局部变量不能重名
  3. 属性的生命周期较长,伴随对象创建而创建,伴随对象销毁而销毁。局部变量生命周期较短,伴随代码块执行而创建,伴随代码块结束而销毁,即生命周期为一次方法调用。
  4. 作用域范围不同:
  • 全局变量(属性):可以被本类或其他类(通过对象调用)使用
  • 局部变量:只能在本类中对应的方法中使用
  1. 修饰符不同:
  • 全局变量(属性)可以加修饰符
  • 局部变量不能加修饰符

this 对象

Java 虚拟机会给每个对象分配一个 this 代表当前对象。

我们可以通过输出变量的 hashCode 来大致认为地址值。

this 的使用细节:

  1. this 关键字可以用来访问本类的属性、方法、构造器
  2. this 用于区分当前类的属性和局部变量
  3. 访问成员方法的语法:`this.方法名(参数列表)``
  4. 访问构造器语法:this(参数列表); 注意!只能在构造器中使用!即只能在一个构造器访问另外一个构造器
  5. this 不能在类定义的外部使用,只能在类定义的方法中使用

方法机制

方法调用机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person {
public void speak() {
System.out.println("我是一个好人);
}
public void cal01(){...}
public void cal02(int n){...}
public int getSum(int num1, int num2){sum = num1 + num2; return sum;}
...
}
...
Person p1 = new Person();
int res = p1.getSum(1, 2);
p1.speak();
p1.cal1();
p1.cal2(5);

png

  1. 当程序执行到方法时,就会开辟一个独立的栈空间
  2. 方法执行完毕或 return 时,就会返回
  3. 返回到调用方法的地方
  4. 继续执行后面的代码

方法传参机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Main {
public static void main(String[] agrs) {
int a = 10;
int b = 20;
A obj = new A();
obj.swap(a, b);
System.out.println("main中: a:" + a + " b:" + b); // a:10 b:20
}
}
class A {
public void swap(int a, int b) {
System.out.println("交换前: a:" + a + " b:" + b); // a:10 b:20
int temp = a;
a = b;
b = temp;
System.out.println("交换后: a:" + a + " b:" + b); // a:20 b:10
}
}

在这段代码中为什么 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Main {
public static void main(String[] agrs) {
Person p = new Person();
p.age = 12;
p.name = "Jack";
B b = new B();
b.test200(p);
// 此时 p 是什么?
}
}
class Person {
String name;
int age;
}
class B {
public void test200(Person p) {
p = null;
}
}

在上面的例子中,执行 b.test200(p) 时,是在 test200栈 中把 p 变为 null, 就失去了和堆的链接,所以对主方法的 p 不影响,main栈 中的 p 还是指向没有修改过的堆。

方法重载 (OverLoad)

Java 中允许同一个类中,多个同名方法的存在,但要求形参列表不一致。例如 System.out.println(); 其中 out 是 PrintStream 类型。

重载的好处:

  • 减轻了起名的麻烦
  • 减轻了记名的麻烦

重载注意事项:

  1. 方法名必须一样
  2. 形参列表必须不一样
  3. 返回类型无要求(但是如果形参相同,那返回类型不同不构成方法重载)

可变参数

Java 允许同一个类中同名同功能参数个数不同的方法封装成一个方法。使用

1
访问修饰符 返回类型 方法名 (数据类型... 方法名)

来定义可变参数。例如:

1
2
3
4
5
6
7
8
9
private int sum(int... nums) {
// 使用可变参数可以看作数组
System.out.println("接受的参数个数=" + nums.length);
int sum = 0;
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
}
return sum;
}

使用注意细节:

  1. 可变参数实参可以为任意个数
  2. 可变参数实参可以为数组
  3. 可变参数实参本质是数组
  4. 可变参数可以和普通类型一起放在形参列表,但是只能放在最后
  5. 一个形参列表中只能出现一个可变形参

构造方法

构造器说明:

  1. 修饰符可以是默认,也可以是 public protected private
  2. 构造器没有返回值
  3. 方法名和类名必须一致
  4. 参数列表与成员方法规则一致
  5. 构造器调用由系统完成
  6. 构造器也可以重载,方法和方法重载相同
  7. 如果没有指定构造器,系统默认生成无参构造器(默认构造器)可以用 javap 命令反编译查看
  8. 一旦定义了构造器,默认构造器被覆盖,不能再使用,除非显式定义

包的基本语法:package 包名

包的三大作用:

  1. 区分相同名字的类
  2. 类多时方便管理类
  3. 控制访问范围

访问修饰符

Java 提供四种访问控制修饰符号,用于控制方法和属性的访问权限

  1. 公开级别:public 对外公开
  2. 受保护级别:protected 对子类和同一个包中的类公开
  3. 默认级别:没有修饰符号,向同一个包中的类公开
  4. 私有级别:private 只有类本身可以访问,不对外公开
访问修饰符 当前类 同一包中的其他类 子类 其他包中的类
private
default (无修饰符)
protected
public

面向对象三大特征

封装 (encapsulation)

封装是把抽象出的数据(属性)和对数据的操作(方法)封装在一起,数据被保护在内部,程序的其它部分只有通过被授权的操作(方法)才能对数据进行操作。

使用封装可以隐藏实现细节,对数据进行验证,保证安全合理。

封装的实现步骤:

  1. 先对属性进行私有化 private , 使外界不能直接修改属性
  2. 提供 public 的 set 方法,用于对属性判断并赋值
1
2
3
4
public void setXxx(类型 参数名) {
// 数据验证的业务逻辑
属性 = 参数名;
}
  1. 提供 public 的 get 方法,用于获得属性值
1
2
3
4
public 类型 getXxx() {
// 业务逻辑
return xxx;
}

这时如果通过构造方法,那设置的封装就无效了,此时我们在构造方法中调用 setter 来保证封装。

继承

继承可以解决代码复用,让编程更加靠近人类思维,当多个类存在相同的属性时可以在类中抽象出父类,在父类中定义相同的属性和方法,所有的子类不需要重新定义这些方法和属性,只需要通过 extends 来表明继承父类。

1
2
3
class 子类 extends 父类 {
...
}

子类会自动拥有(非私有)父类定义的属性和方法,父类又叫超类,基类;子类又叫派生类。

继承使用细节:

  1. 子类继承了所有的属性和方法,但是私有属性和方法不能在子类直接访问,要通过公共方法访问
  2. 子类必须调用父类的构造器,完成父类的初始化
  3. 创造子类对象时,不管使用子类哪个构造器,默认情况总会调用父类的无参构造器,如果父类没有无参构造器,则必须在子类构造器中用 super 指定使用父类构造器完成对父类的初始化工作
  4. 如果希望指定调用父类构造器,则显式调用 super(参数列表);
  5. super 在使用时需要放在构造器第一行
  6. super();this(); 都要放在第一行,因此两个方法不能共存在一个构造器中
  7. Java 中所有类都是 Object类 的子类,Object 是所有类的父类
  8. 父类构造器的调用不限于直接父类,将向上追溯直到 Object 类
  9. Java 是单继承机制,子类最多只能继承一个父类
  10. 不能滥用继承,子类和父类见必须满足 is-a 的逻辑关系
1
2
3
4
5
6
7
8
9
10
11
class GrandPa {
String name = "大头爷爷";
String hobby = "旅游";
}
class Father extends GrandPa {
String name = "大头爸爸";
int age = 39;
}
class Son extends Father {
String name = "大头儿子";
}

此时在 main栈 中运行 Son son = new Son(); 内存中发生了什么?

png

son 访问属性时的流程:先查看子类是否有该属性 -> 若没有就向上父类询问

如果 Father 中的 age 为 private 而 GrandPa 中的 age 为 public 也不能访问到,因为追溯到 Father 的 age 发现为 private 时就直接报错了。

super

super 代表父类的引用,用于访问父类的属性、方法、构造器。 super.方法名 访问父类非私有属性; super.方法名(参数列表); 访问父类非私有方法; super(参数列表); 放在构造器第一句,只能出现一句。

使用 super 的细节:

  1. 调用父类构造器的好处:分工明确(父类属性由父类初始化,子类属性由子类初始化)
  2. 若子类和父类由重名的成员,为了访问父类的成员必须通过 super ,如果没有重名,this、super、直接访问效果一致
  3. super 的访问不限于直接父类,如果 GrandPa 和本类有重名成员,也可以通过 super 访问 GrandPa 成员。如果多个父类中都有同名成员,使用 super 遵循就近原则
区别点 this super
访问属性 访问本类中的属性,如果本类没有从父类查找 访问父类属性
调用方法 访问本类中的方法,如果没有从父类查找 直接访问父类方法
调用构造器 调用本类构造器,必须放在首行 调用父类构造器,必须放在首行
特殊 表示当前对象 子类中访问父类对象
方法重写/覆盖 (override)

方法重写细节:

  1. 子类方法的参数,方法名称,要和父类完全一致
  2. 子类方法的返回类型和父类方法的返回类型一样,或是返回父类返回类型的子类,例如父类返回 Object,子类返回 String
  3. 子类方法不能缩小父类的访问权限
方法重写和方法重载的区别
名称 发生范围 方法名 参数列表 返回类型 修饰符
重载 overload 本类 必须一样 类型,个数或顺序至少有一个不同 无要求 无要求
重写 override 父子类 必须一样 相同 子类重写的方法和父类返回的类型一致,或是父类返回类型的子类 子类方法不能缩小父类的访问范围

多态 (polymorhic)

通过一个例子引出多态:定义一个 feed 方法,因为要处理不同的类型的 Animal 子类,就要写很多个功能一致的函数……

方法和对象具有多种形态,是面向对象的第三特征,多态是建立在继承和封装之上的。

方法的多态

重写和重载体现多态。

对象的多态 (多态的核心)

重要的几句话:

  1. 一个对象的编译类型和运行类型可以不一致
  2. 编译类型在定义对象时就确定了,不能改变
  3. 运行类型可以是变化的
  4. 编译类型看定义时 = 的左边,运行类型看 = 的右边

例如对于以下代码

1
2
3
4
Animal animal = new Dog(); // 编译类型时 Animal,运行类型是 Dog
animal.cry(); // 执行的是 Dog 的 cry()
animal = new Cat(); // 运行类型变为 Cat ,编译类型仍是 Animal
animal.cry(); // 执行的是 Cat 的 cry()
向上转型
  • 本质:父类的引用指向了子类的对象
  • 语法:父类 name = new 子类();
  • 特点:编译类型看左边,运行类型看右边,可以依权限调用父类成员,不能调用子类特有成员(因为在编译阶段能调用的成员由编译类型决定),运行效果看子类
1
Animal animal = new Cat(); // 父类引用指向子类对象

如果访问的内容不在方法区内,就会指向编译类型的堆:

1
2
3
4
// class Base -> int count = 10;
// class Sub extends Base -> int count = 20;
Base base = new Sub();
sout(base.count); // 直接看编译类型
向下转型
  1. 语法:子类类型 name = (子类类型)父类引用;
  2. 只能强转父类的引用,不能强转父类的对象
  3. 要求父类的引用必须指向的是当前目标类型的对象
  4. 向下转型后,可以调用子类类型中的成员
1
2
3
Animal animal = new Cat(); // animal 指向一个 Cat 对象
Cat cat = (Cat)animal; // 新的 cat 指向同一个 Cat 对象
// Dog dog = (Dog) animal 报错!无法将 Cat 对象转换为 Dog
动态绑定机制
  1. 调用方法时,方法会和内存地址/运行类型绑定
  2. 调用对象属性时,没有动态绑定机制,哪里运行哪里使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class A {
public int i = 10;
public int sum() {
return getI() + 10;
}
public int sum1() {
return i + 10;
}
public int getI() {
return i;
}
}
class B extends A{
public int i = 20;
public int getI() {
return i;
}
public int sum1() {
return i + 10;
}
}

...main

A a = new B(); // 向上转型,编译类型为A 运行类型为B
sout(a.sum()); // B 中没有 sum() 追溯到 A 的 sum() 但是 i 仍为 B 中 i -> 30
sout(a.sum1()); // 30

可以通过关键字 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()
  1. 对象被回收时,系统自动调用 finalize 方法。子类可以重写该方法用于释放资源
  2. 当某个对象没有任何引用时,JVM 认为这个对象是一个垃圾对象,使用垃圾回收机制销毁对象,销毁前调用 finalize()
  3. 垃圾回收机制的调用是由系统决定的,也可以通过 System.gc() 主动触发垃圾回收机制‘
1
2
3
Car bwm = new Car("宝马");
bwm = null;
System.gc();

此时 Car 在堆中的对象没有任何引用,就会被系统回收。

从Java9开始,finalize方法已被标注为@Deprecated,也就是过期了!

类变量 (静态变量) 和类方法

是该类所有对象共享的变量,任何实例取到的都是同一个值,修改时也修改的同一个变量。

1
2
访问修饰符 static 数据类型 变量名; [推荐]
static 访问修饰符 数据类型 变量名; [可用]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Child {
// count 变量被所有 Child 实例共享, 类变量可以通过类名访问
public static int count = 0;
public Child(String name) {
this.name = name;
}
public void join() {
sout(name + " 加入了游戏");
}
}

... // main 中
Child child1 = new Child("1");
child1.count++;
Child child2 = new Child("2");
child2.count++;
Child child3 = new Child("3");
child3.count++;

// 此时 Child.count 值为 3

count 在内存中的位置是哪里?有不同的说法

  1. 在方法区中有一个空间叫静态域,count 在静态域中
  2. count 直接存放在堆中

实际上,要根据 JDK 版本确定,在 JDK 7 以前放在方法区中,在之后放在堆中。

类变量使用细节:

  1. 需要让某个类的所有对象共享一个变量时可以用类变量,
  2. 类变量是该类所有实例共享的,实例变量是每个对象独享的
  3. 类变量可以通过 类名.类变量名对象名.类变量名 来访问,但是推荐使用 类名.类变量名

何时使用类方法?当方法中不涉及到任何和对象相关的成员,就可以将方法设计为静态方法,提高开发效率。常用于工具类中,可以直接调用方法。

1
2
访问修饰符 static 返回类型 方法名() {} [推荐]
static 访问修饰符 返回类型 方法名() {} [可行]

调用方法和类变量一致。

理解 main() 方法

  1. main() 方法由 Java 虚拟机调用
  2. Java 虚拟机需要调用 main() 方法,所以访问权限必须是 public
  3. Java 虚拟机在执行 main() 方法时不必创建对象,所以必须是 static
  4. main() 方法接受 String 类型的数组参数,该数组中保存执行 Java 命令传递的类参数
  5. java 执行的程序 参数1 参数...

代码块(初始化块)

属于类中的成员,类似于方法,将逻辑语句封装在方法体中,通过 {} 包围。和方法不同,没有方法名,没有返回,没有参数,只有方法体,而且用显式调用,在加载类或创建对象时调用。

1
2
3
(static) {
...;
}

没有 static 修饰的成为普通代码块/非静态代码块,通过 static 修饰的为静态代码块。

代码块相当于另一种形式的构造器,可以做初始化操作。如果多个构造器中都有重复语句,可以抽取到初始化块中,提升代码重用性。

代码块的使用细节:

  1. 代码块的作用是对类初始化,伴随着类的加载而执行,只执行一次。如果是非静态代码块,在每次创建对象都会执行。
  2. 类什么时候被加载
  • 创建对象实例时 (new)
  • 创建子类对象实例,父类也会被加载
  • 使用类的静态成员时(静态属性,静态方法)
  1. 普通的代码块在创建对象时会被隐式调用,被创建一次就会调用一次,但是使用静态成员时普通代码块不会执行。
  2. 创建对象时,调用顺序
    1. 调用静态代码块静态属性初始化 (优先级一致,按照定义的顺序调用)
    2. 调用普通代码块普通属性的初始化 (优先级一致,按照定义的顺序调用)
    3. 调用构造方法
  3. 构造方法的最前面隐藏了 super(); 和调用普通代码块,而静态代码块和属性是在类加载时执行,所以优先于非静态
  4. 创建一个子类对象时,调用顺序如下
    1. 父类的静态代码块和静态属性初始化(优先级一致,按照定义的顺序调用)
    2. 子类的静态代码块和静态属性初始化(优先级一致,按照定义的顺序调用)
    3. 父类的普通代码块和普通属性初始化(优先级一致,按照定义的顺序调用)
    4. 父类的构造方法
    5. 子类的普通代码块和普通属性初始化(优先级一致,按照定义的顺序调用)
    6. 子类的构造方法
  5. 静态代码块只能直接调用静态成员,普通代码块可以调用任意成员。

final 关键字

使用场景:

  1. 不希望类被继承可以使用 final
  2. 不希望父类的某个方法被重写/覆盖,可以用 final
  3. 不希望类的某个属性被修改,可以用 final
  4. 不希望局部变量被修改,可以用 final

注意事项:

  1. final 修饰的属性又叫做 常量
  2. final 修饰的属性在定义时必须赋初值,并且不能再修改,可以在一下位置赋值
  • 定义时
  • 构造器中
  • 代码块中
  1. final 修饰的属性时静态的话,初始化的位置只能是:
  • 定义时
  • 静态代码块中 (不能再构造器中赋值)
  1. final 类不能继承,但是可以实例化对象
  2. 类不是 final 类,但是又 final 方法,方法不能重写,但是类可以继承
  3. 如果类已经是 final 类,没有必要修饰方法为 final
  4. fina 和 static 通常搭配使用,编译器底层做了优化处理
  5. 包装类(Integer,Double,Float,Boolean)都是 final 类,String 也是 final 类

abstract 关键字

  1. 抽象类用 abstract 修饰类
  2. 用 abstract 修饰方法,就是抽象方法(抽象方法没有方法体)
  3. 抽象类的价值在于设计,设计好后子类继承实现
  4. 抽象类再框架和设计模式中使用较多
  5. 抽象类可以有任意成员(抽象类是类),可以拥有非抽象方法,构造器,静态属性等
  6. 抽象方法不能有主题,即不能实现方法

接口 - interface

1
2
3
4
5
6
7
8
[修饰词] interface [name] {
...
}

[修饰词] class implements [接口名] {
自己的属性和方法;
必须实现接口方法
}

在 JDK7 前,接口中的所有方法都没有方法体,即都是抽象方法,JDK8 后接口类可以有静态方法,默认方法,也就是说接口中可以有具体的方法实现。

注意事项

  1. 接口不能被实例化
  2. 接口中所有方法都是 public 方法,接口中的抽象方法可以不用 abstract 修饰
  3. 普通类实现接口必须将接口所有方法实现
  4. 抽象类实现接口可以不实现方法
  5. 一个类可以实现多个接口 implements IA, IB
  6. 接口中的属性是 public static final 修饰的
  7. 接口属性访问形式和静态属性访问形式相同
  8. 接口不能继承其它类,但是可以继承多个别的接口
  9. 接口的修饰符只能是 public 和 默认,与类的修饰符一致

接口与继承

继承的价值在于解决代码的复用性和可维护性,接口主要价值在于设计规范。

继承是 is-a 的关系,接口是 like-a 的关系。

接口在一定程度上实现代码解耦,即接口规范性和动态绑定。

接口的多态

  1. 接口方法中的参数类型多态,如 Usb 实例既可以接收手机对象,又可以接受相机对象
  2. 多态数组:在 Usb 数组中存放 Phone,Camera 对象
  3. 接口存在多态传递现象——即接口的接口子类被实现后,接口和接口子类的方法都需要被实现

内部类

一个类的内部又完整的嵌套了另一个类结构,被嵌套的类称为内部类 (inner class), 嵌套其它类的类称为外部类 (outer class)。这是类的五大成员之一 (属性,方法,构造器,代码块,内部类) 。内部类的最大特点就是可以直接访问私有属性,并且可以体现类与类之间的包含关系。

1
2
3
4
5
6
class Outer {
...
class Inner {
...
}
}

局部内部类

说明:局部内部类是定义在外部类的局部位置,比如方法中,而且有类名

  1. 可以直接访问外部类的所有成员,包括是非私有的
  2. 不能直接添加访问修饰符,因为它的地位就是局部变量,局部变量不能使用修饰符,但是可以用 final 修饰
  3. 作用域:仅仅在定义它的方法和代码块中
  4. 局部内部类访问外部类成员:直接访问
  5. 外部类访问内部类成员:创建对象再访问
  6. 外部其它类不能访问局部内部类,因为它相当于局部变量
  7. 如果外部类和局部内部类成员重名,遵循就近原则,要访问外部类成员可以使用 外部类名.this.成员。本质:外部类.this 就是外部类对象,谁调用了它,this 就指向谁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Outer02 {
private int n1 = 100;
private void m2() {}
public void m1() { // 方法
final class Inner02 { // 添加 final 后无法被同一个作用域中的其它内部类继承
public void f1() {
// 可以直接访问 n1
sout(n1);
// 可以直接使用 m2
m2();
}
}
// 在作用域中创建对象访问
Inner02 inner02 = new Inner02();
inner02.f1();
}

}

匿名内部类

注意事项:

  1. 基本语法:new 类或接口(参数列表){ ... };
  2. 匿名内部类既是一个类的定义,同时本身也是一个对象,从语法上看既有定义类的特征也有创建对象的特征,可以调用匿名内部类的方法:
1
2
3
4
5
6
new A() {
@Override
public void cry() {
sout("hello");
}
}.cry();
  1. 可以直接访问外部类所有成员,包括私有的
  2. 不能添加访问修饰符,因为地位是局部变量
  3. 作用域:在定义它的方法或代码块中
  4. 访问外部成员:直接访问
  5. 外部其它类:不能访问
  6. 外部类和内部类成员重名使用 this 关键字

成员内部类

成员内部类定义在外部类的成员位置,并且没有static修饰:

  1. 可以直接访问外部类的所有成员,包括私有的
  2. 可以添加任意修饰符,地位相当于成员
  3. 作用域:与成员一样为整个整体。可以在外部类的成员方法中创建内部类对象再调用方法获取
  4. 成员内部类访问外部类:直接访问
  5. 外部类访问成员内部类:创建对象再访问
  6. 外部其它类访问成员内部类
  7. 重名使用 this

静态内部类

成员内部类定义在外部类的成员位置,有static修饰:

  1. 可以直接访问所有的静态成员
  2. 可以添加任意修饰符,地位相当于成员
  3. 作用域:与成员一样为整个整体
  4. 静态内部类访问外部类:直接访问静态成员
  5. 外部类访问静态内部类:创建对象后访问
  6. 重名使用 this

枚举类 (enumeration)

枚举是一组常量的集合,属于一种特殊的类,包含一组有限的特殊对象。

我们可以自定义一个枚举类,只需要让对象静态公开且最终,构造方法私有化来实现。

1
2
3
4
5
6
7
8
9
10
public enum Season {  
SPRING("春天", "温暖"), WINTER("冬天", "寒冷"),
AUTUMN("秋天", "凉爽"), SUMMER("夏天", "炎热");
private final String name;
private String feature;
Season(String name, String feature) {
this.name = name;
this.feature = feature;
}
}
  1. 当我们使用 enum 关键字来开发一个枚举类时,默认会继承 Enum 类,可以通过 javap 命令反编译证明
  2. 传统的 public static final Season SPRING = new Season("春天", "温暖); 简化为 SPRING("春天", "温暖") ,这里必须知道它调用的是哪个构造器
  3. 如果使用无参构造器创建枚举对象,则实参列表和小括号都可以省略
  4. 当有多个枚举对象时使用 , 分隔,最有一个 ; 结尾
  5. 枚举对象必须放在枚举类的行首

枚举类的成员方法

  • toString : Enum 类中重写过了,返回当前的对象名,子类可以重写该方法
  • name : 返回对象名,子类不能重写
  • ordinal : 返回对象的位置号,默认从 0 开始
  • values : 返回枚举类中的所有常量
  • valueOf : 将字符串转换为枚举对象,要求字符串必须为常量名
  • compareTo : 比较枚举常量 (比较的就是位置号)

使用注意事项:

  1. 使用了 enum 关键字后,就不能再继承其它类,因为 enum 会 隐式继承 Enum,而 Java 是单继承机制
  2. 枚举类和普通类一样可以实现接口

基本注解

注解 (Annotation) 也被称为元数据 (Metadata) , 用于修饰解释 包、类、方法、构造器、属性、局部变量等数据信息。和注释一样注解不影响程序逻辑,但是注解可以被编译或运行,相当于嵌入在代码中的补充信息。在 JavaSE 中,注解的使用目的比较简单,往往是用于标记过时的功能,忽略警告等。在 JavaEE 中注解占据了更加重要的角色,例如用来配置应用程序的切面,代替 JavaEE 旧版中遗留的繁冗代码和 XML 配置等。

  • @Override : 限定方法为重写父类方法,只能用于方法
  • @Deprecated : 用于表示某个程序元素已过时
  • SuppressWarning : 抑制编译器警告

@Override

我们查看 Override 的原码,发现为:

1
2
3
4
@Target(ElementType.METHOD)  
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

其中 @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 中的 @RetentionSOURCE,也就是 class 文件和 JVM 中都不会保留该注解
  • @Target 用于指定 Annotation 可以修饰哪些元素。
  • @Documented 用于指定被该元 Annotation 修饰的 Annotation 类将被 javadoc 工具提取成文档,即生成文档时可以看到该注解。定义为 Documented 的注解必须设置 Retention 值为 RUNTIME
  • @Inherited 被它修饰的 Annotation 具有继承性,即子类会自动具有该 Annotation