Java 学习笔记 - 钢钢更新

数组

创建数组

1
2
3
4
5
6
7
8
9
10
11
public class HelloWorld {
public static void main(String[] args) {
//声明一个引用
int[] a;
//创建一个长度是5的数组,并且使用引用a指向该数组
a = new int[5];

int[] b = new int[5]; //声明的同时,指向一个数组

}
}

访问数组

1
2
3
4
5
6
7
8
9
10
11
12
public class HelloWorld {
public static void main(String[] args) {
int[] a;
a = new int[5];

a[0]= 1; //下标0,代表数组里的第一个数
a[1]= 2;
a[2]= 3;
a[3]= 4;
a[4]= 5;
}
}

数组长度

1
2
3
4
5
6
7
8
9
10
11
12
public class HelloWorld {
public static void main(String[] args) {
int[] a;
a = new int[5];

System.out.println(a.length); //打印数组的长度

a[4]=100; //下标4,实质上是“第5个”,即最后一个
a[5]=101; //下标5,实质上是“第6个”,超出范围 ,产生数组下标越界异常

}
}

数组最小值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class HelloWorld {
public static void main(String[] args) {
int[] a = new int[5];
a[0] = (int) (Math.random() * 100);
a[1] = (int) (Math.random() * 100);
a[2] = (int) (Math.random() * 100);
a[3] = (int) (Math.random() * 100);
a[4] = (int) (Math.random() * 100);

System.out.println("数组中的各个随机数是:");
for (int i = 0; i < a.length; i++)
System.out.println(a[i]);

System.out.println("本练习的目的是,找出最小的一个值: ");
}
}

分配空间与赋值分步进行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class HelloWorld {
public static void main(String[] args) {
int[] a = new int[5]; //分配了长度是5的数组,但是没有赋值

//没有赋值,那么就会使用默认值
//作为int类型的数组,默认值是0
System.out.println(a[0]);

//进行赋值

a[0] = 100;
a[1] = 101;
a[2] = 103;
a[3] = 120;
a[4] = 140;
}
}

分配空间,同时赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class HelloWorld {
public static void main(String[] args) {
//写法一: 分配空间同时赋值
int[] a = new int[]{100,102,444,836,3236};

//写法二: 省略了new int[],效果一样
int[] b = {100,102,444,836,3236};

//写法三:同时分配空间,和指定内容
//在这个例子里,长度是3,内容是5个,产生矛盾了
//所以如果指定了数组的内容,就不能同时设置数组的长度
int[] c = new int[3]{100,102,444,836,3236};

}
}

排序

选择法排序

选择法排序的思路:

  • 把第一位和其他所有的进行比较,只要比第一位小的,就换到第一个位置来
  • 比较完后,第一位就是最小的
  • 然后再从第二位和剩余的其他所有进行比较,只要比第二位小,就换到第二个位置来
  • 比较完后,第二位就是第二小的
  • 以此类推

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
public class HelloWorld {
public static void main(String[] args) {
int a [] = new int[]{18,62,68,82,65,9};
//排序前,先把内容打印出来
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
System.out.println(" ");
//选择法排序

//第一步: 把第一位和其他所有位进行比较
//如果发现其他位置的数据比第一位小,就进行交换

for (int i = 1; i < a.length; i++) {
if(a[i]<a[0]){
int temp = a[0];
a[0] = a[i];
a[i] = temp;
}
}
//把内容打印出来
//可以发现,最小的一个数,到了最前面
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
System.out.println(" ");

//第二步: 把第二位的和剩下的所有位进行比较
for (int i = 2; i < a.length; i++) {
if(a[i]<a[1]){
int temp = a[1];
a[1] = a[i];
a[i] = temp;
}
}
//把内容打印出来
//可以发现,倒数第二小的数,到了第二个位置
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
System.out.println(" ");

//可以发现一个规律
//移动的位置是从0 逐渐增加的
//所以可以在外面套一层循环

for (int j = 0; j < a.length-1; j++) {
for (int i = j+1; i < a.length; i++) {
if(a[i]<a[j]){
int temp = a[j];
a[j] = a[i];
a[i] = temp;
}
}
}

//把内容打印出来
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
System.out.println(" ");
}
}

冒泡法排序

冒泡法排序的思路:

  • 第一步:从第一位开始,把相邻两位进行比较
  • 如果发现前面的比后面的大,就把大的数据交换在后面,循环比较完毕后,最后一位就是最大的
  • 第二步: 再来一次,只不过不用比较最后一位
  • 以此类推

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
public class HelloWorld {
public static void main(String[] args) {
int a [] = new int[]{18,62,68,82,65,9};
//排序前,先把内容打印出来
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
System.out.println(" ");
//冒泡法排序

//第一步:从第一位开始,把相邻两位进行比较
//如果发现前面的比后面的大,就把大的数据交换在后面

for (int i = 0; i < a.length-1; i++) {
if(a[i]>a[i+1]){
int temp = a[i];
a[i] = a[i+1];
a[i+1] = temp;
}
}
//把内容打印出来
//可以发现,最大的到了最后面
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
System.out.println(" ");

//第二步: 再来一次,只不过不用比较最后一位
for (int i = 0; i < a.length-2; i++) {
if(a[i]>a[i+1]){
int temp = a[i];
a[i] = a[i+1];
a[i+1] = temp;
}
}
//把内容打印出来
//可以发现,倒数第二大的到了倒数第二个位置
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
System.out.println(" ");

//可以发现一个规律
//后边界在收缩
//所以可以在外面套一层循环

for (int j = 0; j < a.length; j++) {
for (int i = 0; i < a.length-j-1; i++) {
if(a[i]>a[i+1]){
int temp = a[i];
a[i] = a[i+1];
a[i+1] = temp;
}
}
}

//把内容打印出来
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
System.out.println(" ");
}
}

冒泡法,选择法,二叉树 排序比较

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
package collection;

import java.util.Arrays;
import java.util.List;

public class SortCompare {

public static void main(String[] args) {
//初始化随机数
int total = 40000;
System.out.println("初始化一个长度是"+total+"的随机数字的数组");
int[] originalNumbers = new int[total];
for (int i = 0; i < originalNumbers.length; i++) {
originalNumbers[i] = (int)(Math.random()*total);
}
System.out.println("初始化完毕");
System.out.println("接下来分别用3种算法进行排序");

//从初始化了的随机数组复制过来,以保证,每一种排序算法的目标数组,都是一样的
int[] use4sort;

use4sort= Arrays.copyOf(originalNumbers, originalNumbers.length);
int[] sortedNumbersBySelection= performance(new SelectionSort(use4sort),"选择法");

use4sort= Arrays.copyOf(originalNumbers, originalNumbers.length);
int[] sortedNumbersByBubbling=performance(new BubblingSort(use4sort),"冒泡法");

use4sort= Arrays.copyOf(originalNumbers, originalNumbers.length);
int[] sortedNumbersByTree=performance(new TreeSort(use4sort),"二叉树");

System.out.println("查看排序结果,是否是不同的数组对象");
System.out.println(sortedNumbersBySelection);
System.out.println(sortedNumbersByBubbling);
System.out.println(sortedNumbersByTree);

System.out.println("查看排序结果,内容是否相同");
System.out.println("比较 选择法 和 冒泡法 排序结果:");
System.out.println(Arrays.equals(sortedNumbersBySelection, sortedNumbersByBubbling));
System.out.println("比较 选择法 和 二叉树 排序结果:");
System.out.println(Arrays.equals(sortedNumbersBySelection, sortedNumbersByTree));

}

interface Sort{
void sort();
int[] values();
}

static class SelectionSort implements Sort{

int numbers[];
SelectionSort(int [] numbers){
this.numbers = numbers;
}

@Override
public void sort() {
for (int j = 0; j < numbers.length-1; j++) {
for (int i = j+1; i < numbers.length; i++) {
if(numbers[i]<numbers[j]){
int temp = numbers[j];
numbers[j] = numbers[i];
numbers[i] = temp;
}
}
}
}

@Override
public int[] values() {
// TODO Auto-generated method stub
return numbers;
}

}

static class BubblingSort implements Sort{
int numbers[];
BubblingSort(int [] numbers){
this.numbers = numbers;
}
@Override
public void sort() {
for (int j = 0; j < numbers.length; j++) {
for (int i = 0; i < numbers.length-j-1; i++) {
if(numbers[i]>numbers[i+1]){
int temp = numbers[i];
numbers[i] = numbers[i+1];
numbers[i+1] = temp;
}
}
}
}
@Override
public int[] values() {
// TODO Auto-generated method stub
return numbers;
}

}

static class TreeSort implements Sort{
int numbers[];
Node n;

TreeSort(int [] numbers){
n =new Node();
this.numbers = numbers;
}
@Override
public void sort() {

for (int i : numbers) {
n.add(i);
}
}
@Override
public int[] values() {
List<Object> list = n.values();
int sortedNumbers[] = new int[list.size()];
for (int i = 0; i < sortedNumbers.length; i++) {
sortedNumbers[i] = Integer.parseInt(list.get(i).toString());
}
return sortedNumbers;
}
}

private static int[] performance(Sort algorithm, String type) {
long start = System.currentTimeMillis();
algorithm.sort();
int sortedNumbers[] = algorithm.values();
long end = System.currentTimeMillis();
System.out.printf("%s排序,一共耗时 %d 毫秒%n",type,end-start);
return sortedNumbers;
}
}

增强型for循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class HelloWorld {
public static void main(String[] args) {
int values [] = new int[]{18,62,68,82,65,9};
//常规遍历
for (int i = 0; i < values.length; i++) {
int each = values[i];
System.out.println(each);
}

//增强型for循环遍历
for (int each : values) {
System.out.println(each);
}

}
}

复制数组

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
public class HelloWorld {
public static void main(String[] args) {
int a [] = new int[]{18,62,68,82,65,9};

int b[] = new int[3];//分配了长度是3的空间,但是没有赋值

//通过数组赋值把,a数组的前3位赋值到b数组

//方法一: for循环

for (int i = 0; i < b.length; i++) {
b[i] = a[i];
}

//方法二: System.arraycopy(src, srcPos, dest, destPos, length)
//src: 源数组
//srcPos: 从源数组复制数据的起始位置
//dest: 目标数组
//destPos: 复制到目标数组的启始位置
//length: 复制的长度
System.arraycopy(a, 0, b, 0, 3);

//把内容打印出来
for (int i = 0; i < b.length; i++) {
System.out.print(b[i] + " ");
}

}
}

初始化二维数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class HelloWorld {
public static void main(String[] args) {
//初始化二维数组,
int[][] a = new int[2][3]; //有两个一维数组,每个一维数组的长度是3
a[1][2] = 5; //可以直接访问一维数组,因为已经分配了空间

//只分配了二维数组
int[][] b = new int[2][]; //有两个一维数组,每个一维数组的长度暂未分配
b[0] =new int[3]; //必须事先分配长度,才可以访问
b[0][2] = 5;

//指定内容的同时,分配空间
int[][] c = new int[][]{
{1,2,4},
{4,5},
{6,7,8,9}
};

}
}

JAVA.UTIL.ARRAYS类常用方法

数组复制

  • 与使用System.arraycopy进行数组复制类似的, Arrays提供了一个copyOfRange方法进行数组复制。
  • 不同的是System.arraycopy,需要事先准备好目标数组,并分配长度。 copyOfRange 只需要源数组就就可以了,通过返回值,就能够得到目标数组了。
  • 除此之外,需要注意的是 copyOfRange 的第3个参数,表示源数组的结束位置,是取不到的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.Arrays;

public class HelloWorld {
public static void main(String[] args) {
int a[] = new int[] { 18, 62, 68, 82, 65, 9 };

// copyOfRange(int[] original, int from, int to)
// 第一个参数表示源数组
// 第二个参数表示开始位置(取得到)
// 第三个参数表示结束位置(取不到)
int[] b = Arrays.copyOfRange(a, 0, 3);

for (int i = 0; i < b.length; i++) {
System.out.print(b[i] + " ");
}

}
}

转换为字符串

1
2
3
4
5
6
7
8
9
10
import java.util.Arrays;

public class HelloWorld {
public static void main(String[] args) {
int a[] = new int[] { 18, 62, 68, 82, 65, 9 };
String content = Arrays.toString(a);
System.out.println(content);

}
}

数组排序

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.Arrays;

public class HelloWorld {
public static void main(String[] args) {
int a[] = new int[] { 18, 62, 68, 82, 65, 9 };
System.out.println("排序之前 :");
System.out.println(Arrays.toString(a));
Arrays.sort(a);
System.out.println("排序之后:");
System.out.println(Arrays.toString(a));

}
}

数组搜索

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.Arrays;

public class HelloWorld {
public static void main(String[] args) {
int a[] = new int[] { 18, 62, 68, 82, 65, 9 };

Arrays.sort(a);

System.out.println(Arrays.toString(a));
//使用binarySearch之前,必须先使用sort进行排序
System.out.println("数字 62出现的位置:"+Arrays.binarySearch(a, 62));
}
}

数组判断是否相同

1
2
3
4
5
6
7
8
9
10
import java.util.Arrays;

public class HelloWorld {
public static void main(String[] args) {
int a[] = new int[] { 18, 62, 68, 82, 65, 9 };
int b[] = new int[] { 18, 62, 68, 82, 65, 8 };

System.out.println(Arrays.equals(a, b));
}
}

数组填充

1
2
3
4
5
6
7
8
9
10
11
12
import java.util.Arrays;

public class HelloWorld {
public static void main(String[] args) {
int a[] = new int[10];

Arrays.fill(a, 5);

System.out.println(Arrays.toString(a));

}
}

类和对象

引用

物品类Item

物品类Item 有属性 name,price

1
2
3
4
public class Item {
String name;
int price;
}

武器类Weapon(不继承)

武器类: Weapon不继承Item的写法
独立设计 name和price属性
同时多了一个属性 damage 攻击力

1
2
3
4
5
6
public class Weapon{
String name;
int price;
int damage; //攻击力

}

武器类Weapon(继承类Item)

这一次Weapon继承Item
虽然Weapon自己没有设计name和price,但是通过继承Item类,也具备了name和price属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Weapon extends Item{
int damage; //攻击力

public static void main(String[] args) {
Weapon infinityEdge = new Weapon();
infinityEdge.damage = 65; //damage属性在类Weapon中新设计的

infinityEdge.name = "无尽之刃";//name属性,是从Item中继承来的,就不需要重复设计了
infinityEdge.price = 3600;

}

}

方法重载

attack方法的重载

有一种英雄,叫做物理攻击英雄 ADHero
为ADHero 提供三种方法

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 ADHero extends Hero {
public void attack() {
System.out.println(name + " 进行了一次攻击 ,但是不确定打中谁了");
}

public void attack(Hero h1) {
System.out.println(name + "对" + h1.name + "进行了一次攻击 ");
}

public void attack(Hero h1, Hero h2) {
System.out.println(name + "同时对" + h1.name + "和" + h2.name + "进行了攻击 ");
}

public static void main(String[] args) {
ADHero bh = new ADHero();
bh.name = "赏金猎人";

Hero h1 = new Hero();
h1.name = "盖伦";
Hero h2 = new Hero();
h2.name = "提莫";

bh.attack(h1);
bh.attack(h1, h2);
}

}

可变数量的参数

如果要攻击更多的英雄,就需要设计更多的方法,这样类会显得很累赘,像这样:

1
2
3
public void attack(Hero h1)
public void attack(Hero h1,Hero h2)
public void attack(Hero h1,Hero h2,Hero h3)

这时,可以采用可变数量的参数
只需要设计一个方法

public void attack(Hero ...heros)

即可代表上述所有的方法了
在方法里,使用操作数组的方式处理参数heros即可

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
public class ADHero extends Hero {

public void attack() {
System.out.println(name + " 进行了一次攻击 ,但是不确定打中谁了");
}

// 可变数量的参数
public void attack(Hero... heros) {
for (int i = 0; i < heros.length; i++) {
System.out.println(name + " 攻击了 " + heros[i].name);

}
}

public static void main(String[] args) {
ADHero bh = new ADHero();
bh.name = "赏金猎人";

Hero h1 = new Hero();
h1.name = "盖伦";
Hero h2 = new Hero();
h2.name = "提莫";

bh.attack(h1);
bh.attack(h1, h2);

}

}

构造方法

方法名和类名一样(包括大小写)
没有返回类型
实例化一个对象的时候,必然调用构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Hero {

String name;

float hp;

float armor;

int moveSpeed;

// 方法名和类名一样(包括大小写)
// 没有返回类型
public Hero() {
System.out.println("实例化一个对象的时候,必然调用构造方法");
}

public static void main(String[] args) {
//实例化一个对象的时候,必然调用构造方法
Hero h = new Hero();
}

}

隐式的构造方法

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
public class Hero {

String name; //姓名

float hp; //血量

float armor; //护甲

int moveSpeed; //移动速度

//这个无参的构造方法,如果不写,
//就会默认提供一个无参的构造方法
// public Hero(){
// System.out.println("调用Hero的构造方法");
// }

public static void main(String[] args) {
Hero garen = new Hero();
garen.name = "盖伦";
garen.hp = 616.28f;
garen.armor = 27.536f;
garen.moveSpeed = 350;

Hero teemo = new Hero();
teemo.name = "提莫";
teemo.hp = 383f;
teemo.armor = 14f;
teemo.moveSpeed = 330;
}

}

有参的构造方法

一旦提供了一个有参的构造方法
同时又没有显式的提供一个无参的构造方法
那么默认的无参的构造方法,就“木有了“

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

String name; //姓名

float hp; //血量

float armor; //护甲

int moveSpeed; //移动速度

//有参的构造方法
//默认的无参的构造方法就失效了
public Hero(String heroname){
name = heroname;
}

public static void main(String[] args) {
Hero garen = new Hero("盖伦");

Hero teemo = new Hero(); //无参的构造方法“木有了”
}

}

构造方法的重载

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 Hero {

String name; //姓名

float hp; //血量

float armor; //护甲

int moveSpeed; //移动速度

//带一个参数的构造方法
public Hero(String heroname){
name = heroname;
}

//带两个参数的构造方法
public Hero(String heroname,float herohp){
name = heroname;
hp = herohp;
}

public static void main(String[] args) {
Hero garen = new Hero("盖伦");
Hero teemo = new Hero("提莫",383);
}

}

this

this代表当前对象

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
public class Hero {

String name; //姓名

float hp; //血量

float armor; //护甲

int moveSpeed; //移动速度

//打印内存中的虚拟地址
public void showAddressInMemory(){
System.out.println("打印this看到的虚拟地址:"+this);
}

public static void main(String[] args) {
Hero garen = new Hero();
garen.name = "盖伦";
//直接打印对象,会显示该对象在内存中的虚拟地址
//格式:Hero@c17164 c17164即虚拟地址,每次执行,得到的地址不一定一样

System.out.println("打印对象看到的虚拟地址:"+garen);
//调用showAddressInMemory,打印该对象的this,显示相同的虚拟地址
garen.showAddressInMemory();

Hero teemo = new Hero();
teemo.name = "提莫";
System.out.println("打印对象看到的虚拟地址:"+teemo);
teemo.showAddressInMemory();
}

}

通过this访问属性

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
public class Hero {

String name; //姓名

float hp; //血量

float armor; //护甲

int moveSpeed; //移动速度

//参数名和属性名一样
//在方法体中,只能访问到参数name
public void setName1(String name){
name = name;
}

//为了避免setName1中的问题,参数名不得不使用其他变量名
public void setName2(String heroName){
name = heroName;
}

//通过this访问属性
public void setName3(String name){
//name代表的是参数name
//this.name代表的是属性name
this.name = name;
}

public static void main(String[] args) {
Hero h =new Hero();

h.setName1("teemo");
System.out.println(h.name);

h.setName2("garen");
System.out.println(h.name);

h.setName3("死歌");
System.out.println(h.name);
}

}

通过this调用其他的构造方法

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
public class Hero {

String name; //姓名

float hp; //血量

float armor; //护甲

int moveSpeed; //移动速度

//带一个参数的构造方法
public Hero(String name){
System.out.println("一个参数的构造方法");
this.name = name;
}

//带两个参数的构造方法
public Hero(String name,float hp){
this(name);
System.out.println("两个参数的构造方法");
this.hp = hp;
}

public static void main(String[] args) {
Hero teemo = new Hero("提莫",383);

System.out.println(teemo.name);

}

}

传参

基本类型传参

在方法内,无法修改方法外的基本类型参数

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
public class Hero {

String name; //姓名

float hp; //血量

float armor; //护甲

int moveSpeed; //移动速度

public Hero(){

}

//回血
public void huixue(int xp){
hp = hp + xp;
//回血完毕后,血瓶=0
xp=0;
}

public Hero(String name,float hp){
this.name = name;
this.hp = hp;
}

public static void main(String[] args) {
Hero teemo = new Hero("提莫",383);
//血瓶,其值是100
int xueping = 100;

//提莫通过这个血瓶回血

teemo.huixue(xueping);

System.out.println(xueping);

}

}

引用与等号

  • 如果一个变量是基本类型
    比如int hp = 50;
    我们就直接管hp叫变量
    =表示赋值的意思
  • 如果一个变量是类类型
    比如 Hero h = new Hero();
    我们就管h叫做引用。
    =不再是赋值的意思
    =表示指向的意思
    比如: Hero h = new Hero();
    这句话的意思是
    引用h,指向一个Hero对象

类类型传参

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
public class Hero {

String name; // 姓名

float hp; // 血量

float armor; // 护甲

int moveSpeed; // 移动速度

public Hero(String name, float hp) {
this.name = name;
this.hp = hp;
}

// 攻击一个英雄,并让他掉damage点血
public void attack(Hero hero, int damage) {
hero.hp = hero.hp - damage;
}

public static void main(String[] args) {
Hero teemo = new Hero("提莫", 383);
Hero garen = new Hero("盖伦", 616);
garen.attack(teemo, 100);
System.out.println(teemo.hp);
}

}

类之间的关系

类和类之间的关系有如下几种:
以Hero为例

  • 自身:指的是Hero自己
  • 同包子类:ADHero这个类是Hero的子类,并且和Hero处于同一个包下
  • 不同包子类:Support这个类是Hero的子类,但是在另一个包下
  • 同包类: GiantDragon 这个类和Hero是同一个包,但是彼此没有继承关系
  • 其他类:Item这个类,在不同包,也没有继承关系的类

private 私有的

红色字体,表示不可行
Alt text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package charactor;

import property.Weapon;

public class Hero {

//属性id是private的,只有Hero自己可以访问
//子类不能继承
//其他类也不能访问
private int id;

String name;

float hp;

float armor;

int moveSpeed;

public void equip(Weapon w) {

}

}

package/friendly/default 不写

没有修饰符即代表package/friendly/default
float maxHP; 血量上限

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
package charactor;

import property.Weapon;

public class Hero {
private int id;

String name;

// 无修饰符的属性 hp
// 自己可以访问

// 同包子类可以继承
// 不同包子类不能继承

// 同包类可以访问
// 不同包类不能访问
float hp;

float armor;

int moveSpeed;

public void equip(Weapon w) {

}

}

protected 受保护的

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
package charactor;

import property.Weapon;

public class Hero {
private int id;

String name;

// protected饰符的属性 hp
// 自己可以访问

// 同包子类可以继承
// 不同包子类可以继承

// 同包类可以访问
// 不同包类不能访问
protected float hp;

float armor;

int moveSpeed;

public void equip(Weapon w) {

}

}

public 公共的

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
package charactor;

import property.Weapon;

public class Hero {
private int id;

// public的属性 name
// 自己可以访问

// 同包子类可以继承
// 不同包子类可以继承

// 同包类可以访问
// 不同包类可以访问
public String name;

protected float hp;

float armor;

int moveSpeed;

public void equip(Weapon w) {

}

}

修饰符小结

那么什么情况该用什么修饰符呢?

从作用域来看,public能够使用所有的情况。 但是大家在工作的时候,又不会真正全部都使用public,那么到底什么情况该用什么修饰符呢?

  1. 属性通常使用private封装起来
  2. 方法一般使用public用于被调用
  3. 会被子类继承的方法,通常使用protected
  4. package用的不多,一般新手会用package,因为还不知道有修饰符这个东西

再就是作用范围最小原则
简单说,能用private就用private,不行就放大一级,用package,再不行就用protected,最后用public。 这样就能把数据尽量的封装起来,没有必要露出来的,就不用露出来了。

类属性

  • 当一个属性被static修饰的时候,就叫做类属性,又叫做静态属性
  • 当一个属性被声明成类属性,那么所有的对象,都共享一个值。

对象属性对比:

  • 不同对象的 对象属性 的值都可能不一样。
    比如:盖伦的hp 和 提莫的hp 是不一样的,但是所有对象的类属性的值,都是一样的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package charactor;

public class Hero {
public String name; //实例属性,对象属性,非静态属性
protected float hp;
static String copyright;//类属性,静态属性

public static void main(String[] args) {
Hero garen = new Hero();
garen.name = "盖伦";

Hero.copyright = "版权由Riot Games公司所有";

System.out.println(garen.name);
System.out.println(garen.copyright);

Hero teemo = new Hero();
teemo.name = "提莫";
System.out.println(teemo.name);
System.out.println(teemo.copyright);

}

}

访问类属性

访问类属性有两种方式:

  1. 对象.类属性
    teemo.copyright
  2. 类.类属性
    Hero.copyright

这两种方式都可以访问类属性,访问即修改和获取,但是建议使用第二种 类.类属性 的方式进行,这样更符合语义上的理解。

什么时候使用对象属性?什么时候使用类属性?

  • 如果一个属性,每个英雄都不一样,比如name,这样的属性就应该设计为对象属性,因为它是跟着对象走的,每个对象的name都是不同的。
  • 如果一个属性,所有的英雄都共享,都是一样的,那么就应该设计为类属性。比如血量上限,所有的英雄的血量上限都是 9999,不会因为英雄不同,而取不同的值。 这样的属性,就适合设计为类属性。

静态方法

  • 类方法: 又叫做静态方法。访问类方法,不需要对象的存在,直接就访问。
  • 对象方法: 又叫实例方法,非静态方法。访问一个对象方法,必须建立在有一个对象的前提的基础上。
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
package charactor;

public class Hero {
public String name;
protected float hp;

//实例方法,对象方法,非静态方法
//必须有对象才能够调用
public void die(){
hp = 0;
}

//类方法,静态方法
//通过类就可以直接调用
public static void battleWin(){
System.out.println("battle win");
}

public static void main(String[] args) {
Hero garen = new Hero();
garen.name = "盖伦";
//必须有一个对象才能调用
garen.die();

Hero teemo = new Hero();
teemo.name = "提莫";

//无需对象,直接通过类调用
Hero.battleWin();

}
}

调用类方法

和访问类属性一样,调用类方法也有两种方式:

  1. 对象.类方法
    garen.battleWin();
  1. 类.类方法
    Hero.battleWin();

这两种方式都可以调用类方法,但是建议使用第二种 类.类方法 的方式进行,这样更符合语义上的理解。
并且在很多时候,并没有实例,比如在前面练习的时候用到的随机数的获取办法
Math.random()

random()就是一个类方法,直接通过类Math进行调用,并没有一个Math的实例存在。

什么时候设计对象方法? 什么时候设计类方法?

如果在某一个方法里,调用了对象属性,比如

1
2
3
public String getName(){
return name;
}

name属性是对象属性,只有存在一个具体对象的时候,name才有意义。 如果方法里访问了对象属性,那么这个方法,就必须设计为对象方法

如果一个方法,没有调用任何对象属性,那么就可以考虑设计为类方法,比如

1
2
3
public static void printGameDuration(){
System.out.println("已经玩了10分50秒");
}

printGameDuration 打印当前玩了多长时间了,不和某一个具体的英雄关联起来,所有的英雄都是一样的。 这样的方法,更带有功能性色彩
就像取随机数一样,random()是一个功能用途的方法
Math.random()

属性初始化

对象属性初始化有3种:

  1. 声明该属性的时候初始化
  2. 构造方法中初始化
  3. 初始化块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package charactor;

public class Hero {
public String name = "some hero"; //声明该属性的时候初始化
protected float hp;
float maxHP;

{
maxHP = 200; //初始化块
}

public Hero(){
hp = 100; //构造方法中初始化

}

}

单例模式

单例模式又叫做 Singleton模式,指的是一个类,在一个JVM里,只有一个实例存在。

饿汉式单例模式

GiantDragon 应该只有一只,通过私有化其构造方法,使得外部无法通过new 得到新的实例。
GiantDragon 提供了一个public static的getInstance方法,外部调用者通过该方法获取12行定义的对象,而且每一次都是获取同一个对象。 从而达到单例的目的。
这种单例模式又叫做饿汉式单例模式,无论如何都会创建一个实例

GiantDragon.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package charactor;

public class GiantDragon {

//私有化构造方法使得该类无法在外部通过new 进行实例化
private GiantDragon(){

}

//准备一个类属性,指向一个实例化对象。 因为是类属性,所以只有一个

private static GiantDragon instance = new GiantDragon();

//public static 方法,提供给调用者获取12行定义的对象
public static GiantDragon getInstance(){
return instance;
}

}

TestGiantDragon.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package charactor;

public class TestGiantDragon {

public static void main(String[] args) {
//通过new实例化会报错
// GiantDragon g = new GiantDragon();

//只能通过getInstance得到对象

GiantDragon g1 = GiantDragon.getInstance();
GiantDragon g2 = GiantDragon.getInstance();
GiantDragon g3 = GiantDragon.getInstance();

//都是同一个对象
System.out.println(g1==g2);
System.out.println(g1==g3);
}
}

懒汉式单例模式

懒汉式单例模式与饿汉式单例模式不同,只有在调用getInstance的时候,才会创建实例。

GiantDragon.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package charactor;

public class GiantDragon {

//私有化构造方法使得该类无法在外部通过new 进行实例化
private GiantDragon(){
}

//准备一个类属性,用于指向一个实例化对象,但是暂时指向null
private static GiantDragon instance;

//public static 方法,返回实例对象
public static GiantDragon getInstance(){
//第一次访问的时候,发现instance没有指向任何对象,这时实例化一个对象
if(null==instance){
instance = new GiantDragon();
}
//返回 instance指向的对象
return instance;
}

}

TestGiantDragon.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package charactor;

public class TestGiantDragon {

public static void main(String[] args) {
//通过new实例化会报错
// GiantDragon g = new GiantDragon();

//只能通过getInstance得到对象

GiantDragon g1 = GiantDragon.getInstance();
GiantDragon g2 = GiantDragon.getInstance();
GiantDragon g3 = GiantDragon.getInstance();

//都是同一个对象
System.out.println(g1==g2);
System.out.println(g1==g3);
}
}

什么时候使用饿汉式?什么时候使用懒汉式?

  • 饿汉式是立即加载的方式,无论是否会用到这个对象,都会加载。
    如果在构造方法里写了性能消耗较大,占时较久的代码,比如建立与数据库的连接,那么就会在启动的时候感觉稍微有些卡顿。
  • 懒汉式是延迟加载的方式,只有使用的时候才会加载。 并且有线程安全的考量。
    使用懒汉式,在启动的时候,会感觉到比饿汉式略快,因为并没有做对象的实例化。 但是在第一次调用的时候,会进行实例化操作,感觉上就略慢。

看业务需求,如果业务上允许有比较充分的启动和初始化时间,就使用饿汉式,否则就使用懒汉式。

单例模式三元素(什么是单例模式?)

这个是面试的时候经常会考的点,面试题通常的问法是:
什么是单例模式?

回答的时候,要答到三元素:

  1. 构造方法私有化;
  2. 静态属性指向实例;
  3. public static的 getInstance方法,返回第二步的静态属性;

枚举类型

枚举enum是一种特殊的类(还是类),使用枚举可以很方便的定义常量
比如设计一个枚举类型 季节,里面有4种常量。

Season.java

1
2
3
public enum Season {
SPRING,SUMMER,AUTUMN,WINTER
}

HelloWorld.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class HelloWorld {
public static void main(String[] args) {
Season season = Season.SPRING;
switch (season) {
case SPRING:
System.out.println("春天");
break;
case SUMMER:
System.out.println("夏天");
break;
case AUTUMN:
System.out.println("秋天");
break;
case WINTER:
System.out.println("冬天");
break;
}
}
}

使用枚举的好处

假设在使用switch的时候,不是使用枚举,而是使用int,而int的取值范围就不只是1-4,有可能取一个超出1-4之间的值,这样判断结果就似是而非了(因为只有4个季节)。
但是使用枚举,就能把范围死死的限定在SPRING, SUMMER, AUTUMN, WINTER当中,而不会出现奇怪的第5季

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class HelloWorld {
public static void main(String[] args) {
int season = 5;
switch (season) {
case 1:
System.out.println("春天");
break;
case 2:
System.out.println("夏天");
break;
case 3:
System.out.println("秋天");
break;
case 4:
System.out.println("冬天");
break;
}
}
}

遍历枚举

1
2
3
4
5
6
7
public class HelloWorld {
public static void main(String[] args) {
for (Season s : Season.values()) {
System.out.println(s);
}
}
}

接口与继承

接口

在设计LOL的时候,进攻类英雄有两种,一种是进行物理系攻击,一种是进行魔法系攻击
这时候,就可以使用接口来实现这个效果。
接口就像是一种约定,我们约定某些英雄是物理系英雄,那么他们就一定能够进行物理攻击。

物理攻击接口

创建一个接口 File->New->Interface
AD ,声明一个方法 physicAttack 物理攻击,但是没有方法体,是一个“空”方法。

1
2
3
4
5
6
package charactor;

public interface AD {
//物理伤害
public void physicAttack();
}

设计一类英雄,能够使用物理攻击

设计一类英雄,能够使用物理攻击,这类英雄在LOL中被叫做AD类:ADHero

继承了Hero 类,所以继承了name, hp, armor等属性。
实现某个接口,就相当于承诺了某种约定。

所以,实现了AD这个接口,就必须提供AD接口中声明的方法physicAttack()
实现在语法上使用关键字 implements

1
2
3
4
5
6
7
8
9
10
package charactor;

public class ADHero extends Hero implements AD{

@Override
public void physicAttack() {
System.out.println("进行物理攻击");
}

}

魔法攻击接口

1
2
3
4
5
6
package charactor;

public interface AP {

public void magicAttack();
}

设计一类英雄,只能使用魔法攻击

1
2
3
4
5
6
7
8
9
10
package charactor;

public class APHero extends Hero implements AP{

@Override
public void magicAttack() {
System.out.println("进行魔法攻击");
}

}

设计一类英雄,既能进行物理攻击,又能进行魔法攻击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package charactor;

//同时能进行物理和魔法伤害的英雄
public class ADAPHero extends Hero implements AD,AP{

@Override
public void magicAttack() {
System.out.println("进行魔法攻击");
}

@Override
public void physicAttack() {
System.out.println("进行物理攻击");
}

}

什么样的情况下该使用接口?

如上的例子,似乎要接口,不要接口,都一样的,那么接口的意义是什么呢?

学习一个知识点,是由浅入深得进行的。 这里呢,只是引入了接口的概念,要真正理解接口的好处,需要更多的实践,以及在较为复杂的系统中进行大量运用之后,才能够真正理解,比如在学习了多态之后就能进一步加深理解。

对象转型

instanceof

判断一个引用所指向的对象,是否是Hero类型,或者Hero的子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package charactor;

public class Hero {
public String name;
protected float hp;

public static void main(String[] args) {
ADHero ad = new ADHero();
APHero ap = new APHero();

Hero h1= ad;
Hero h2= ap;

//判断引用h1指向的对象,是否是ADHero类型
System.out.println(h1 instanceof ADHero);

//判断引用h2指向的对象,是否是APHero类型
System.out.println(h2 instanceof APHero);

//判断引用h1指向的对象,是否是Hero的子类型
System.out.println(h1 instanceof Hero);
}
}

重写(覆盖Override)

子类可以继承父类的对象方法。在继承后,重复提供该方法,就叫做方法的重写,又叫覆盖 override

父类Item

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package property;

public class Item {
String name;
int price;

public void buy(){
System.out.println("购买");
}
public void effect() {
System.out.println("物品使用后,可以有效果");
}

}

子类LifePotion

1
2
3
4
5
6
7
8
9
package property;

public class LifePotion extends Item{

public void effect(){
System.out.println("血瓶使用后,可以回血");
}

}

调用重写的方法

调用就会执行重写的方法,而不是从父类的方法。所以LifePotion的effect会打印:”血瓶使用后,可以回血”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package property;

public class Item {
String name;
int price;

public void effect(){
System.out.println("物品使用后,可以有效果");
}

public static void main(String[] args) {
Item i = new Item();
i.effect();

LifePotion lp =new LifePotion();
lp.effect();
}

}

如果没有重写这样的机制怎么样?

如果没有重写这样的机制,也就是说LifePotion这个类,一旦继承了Item,所有方法都不能修改了。

但是LifePotion又希望提供一点不同的功能,为了达到这个目的,只能放弃继承Item,重新编写所有的属性和方法,然后在编写effect的时候,做一点小改动。

这样就增加了开发时间和维护成本。

Item.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package property;

public class Item {
String name;
int price;

public void buy(){
System.out.println("购买");
}
public void effect() {
System.out.println("物品使用后,可以有效果");
}

}

LifePotion.java

1
2
3
4
5
6
7
8
9
10
11
12
13
package property;

public class LifePotion {
String name;
int price;

public void buy(){
System.out.println("购买");
}
public void effect(){
System.out.println("血瓶使用后,可以回血");
}
}

多态

操作符的多态

同一个操作符在不同情境下,具备不同的作用。

  • 如果+号两侧都是整型,那么+代表 数字相加;
  • 如果+号两侧,任意一个是字符串,那么+代表字符串连接;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package charactor;

public class Hero {
public String name;
protected float hp;

public static void main(String[] args) {

int i = 5;
int j = 6;
int k = i+j; //如果+号两侧都是整型,那么+代表 数字相加

System.out.println(k);

int a = 5;
String b = "5";

String c = a+b; //如果+号两侧,任意一个是字符串,那么+代表字符串连接
System.out.println(c);

}

}

观察类的多态现象

观察类的多态现象:

  1. i1和i2都是Item类型
  2. 都调用effect方法
  3. 输出不同的结果

多态: 都是同一个类型,调用同一个方法,却能呈现不同的状态。

Item.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package property;

public class Item {
String name;
int price;

public void buy(){
System.out.println("购买");
}
public void effect() {
System.out.println("物品使用后,可以有效果 ");
}

public static void main(String[] args) {
Item i1= new LifePotion();
Item i2 = new MagicPotion();
System.out.print("i1 是Item类型,执行effect打印:");
i1.effect();
System.out.print("i2也是Item类型,执行effect打印:");
i2.effect();
}

}

LifePotion.java

1
2
3
4
5
6
7
package property;

public class LifePotion extends Item {
public void effect(){
System.out.println("血瓶使用后,可以回血");
}
}

MagicPotion.java

1
2
3
4
5
6
7
8
package property;

public class MagicPotion extends Item{

public void effect(){
System.out.println("蓝瓶使用后,可以回魔法");
}
}

类的多态条件

要实现类的多态,需要如下条件

  1. 父类(接口)引用指向子类对象
  2. 调用的方法有重写

类的多态-不使用多态

如果不使用多态,
假设英雄要使用血瓶和魔瓶,就需要为Hero设计两个方法
useLifePotion
useMagicPotion

除了血瓶和魔瓶还有很多种物品,那么就需要设计很多很多个方法,比如
usePurityPotion 净化药水
useGuard 守卫
useInvisiblePotion 使用隐形药水
等等等等

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
package charactor;

import property.LifePotion;
import property.MagicPotion;

public class Hero {
public String name;
protected float hp;

public void useLifePotion(LifePotion lp){
lp.effect();
}
public void useMagicPotion(MagicPotion mp){
mp.effect();
}

public static void main(String[] args) {

Hero garen = new Hero();
garen.name = "盖伦";

LifePotion lp =new LifePotion();
MagicPotion mp =new MagicPotion();

garen.useLifePotion(lp);
garen.useMagicPotion(mp);

}

}

类的多态-使用多态

如果物品的种类特别多,那么就需要设计很多的方法
比如useArmor,useWeapon等等

这个时候采用多态来解决这个问题
设计一个方法叫做useItem,其参数类型是Item
如果是使用血瓶,调用该方法
如果是使用魔瓶,还是调用该方法
无论英雄要使用什么样的物品,只需要一个方法即可。

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
package charactor;

import property.Item;
import property.LifePotion;
import property.MagicPotion;

public class Hero {
public String name;
protected float hp;

public void useItem(Item i){
i.effect();
}

public static void main(String[] args) {

Hero garen = new Hero();
garen.name = "盖伦";

LifePotion lp =new LifePotion();
MagicPotion mp =new MagicPotion();

garen.useItem(lp);
garen.useItem(mp);

}

}

隐藏

父类

父类有一个类方法 :battleWin

1
2
3
4
5
6
7
8
9
10
11
12
13
package charactor;

public class Hero {
public String name;
protected float hp;

//类方法,静态方法
//通过类就可以直接调用
public static void battleWin(){
System.out.println("hero battle win");
}

}

子类隐藏父类的类方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package charactor;

public class ADHero extends Hero implements AD{

@Override
public void physicAttack() {
System.out.println("进行物理攻击");
}

//隐藏父类的battleWin方法
public static void battleWin(){
System.out.println("ad hero battle win");
}

public static void main(String[] args) {
Hero.battleWin();
ADHero.battleWin();
}

}

super

准备一个显式提供无参构造方法的父类

准备显式提供无参构造方法的父类
在实例化Hero对象的时候,其构造方法会打印
“Hero的构造方法 “

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
package charactor;

import property.Item;

public class Hero {

String name; //姓名

float hp; //血量

float armor; //护甲

int moveSpeed; //移动速度

public void useItem(Item i){
System.out.println("hero use item");
i.effect();
}

public Hero(){
System.out.println("Hero的构造方法 ");
}

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

}

实例化子类,父类的构造方法一定会被调用

实例化一个ADHero(), 其构造方法会被调用
父类的构造方法也会被调用
并且是父类构造方法先调用
子类构造方法会默认调用父类的 无参的构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package charactor;

public class ADHero extends Hero implements AD{

@Override
public void physicAttack() {
System.out.println("进行物理攻击");
}

public ADHero(){

System.out.println("AD Hero的构造方法");
}

public static void main(String[] args) {

new ADHero();

}

}

父类显式提供两个构造方法

分别是无参的构造方法和带一个参数的构造方法

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
package charactor;

import property.Item;

public class Hero {

String name; //姓名

float hp; //血量

float armor; //护甲

int moveSpeed; //移动速度

public void useItem(Item i){
System.out.println("hero use item");
i.effect();
}

public Hero(){
System.out.println("Hero的无参的构造方法 ");
}

public Hero(String name){
System.out.println("Hero的有一个参数的构造方法 ");
this.name = name;
}

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

}

子类显式调用父类带参构造方法

使用关键字super 显式调用父类带参的构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package charactor;

public class ADHero extends Hero implements AD{

@Override
public void physicAttack() {
System.out.println("进行物理攻击");
}

public ADHero(String name){
super(name);
System.out.println("AD Hero的构造方法");
}

public static void main(String[] args) {
new ADHero("德莱文");
}

}

调用父类属性

通过super调用父类的moveSpeed属性
ADHero也提供了属性moveSpeed

1
2
3
4
5
6
public int getMoveSpeed(){
return this.moveSpeed;
}
public int getMoveSpeed2(){
return super.moveSpeed;
}

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
package charactor;

public class ADHero extends Hero implements AD{

int moveSpeed=400; //移动速度

@Override
public void physicAttack() {
System.out.println("进行物理攻击");
}

public int getMoveSpeed(){
return this.moveSpeed;
}

public int getMoveSpeed2(){
return super.moveSpeed;
}

public static void main(String[] args) {
ADHero h= new ADHero();

System.out.println(h.getMoveSpeed());
System.out.println(h.getMoveSpeed2());

}

}

调用父类方法

ADHero重写了useItem方法,并且在useItem中通过super调用父类的useItem方法

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
package charactor;

import property.Item;
import property.LifePotion;

public class ADHero extends Hero implements AD {

int moveSpeed = 400; // 移动速度

@Override
public void physicAttack() {
System.out.println("进行物理攻击");
}

public int getMoveSpeed() {
return this.moveSpeed;
}

public int getMoveSpeed2() {
return super.moveSpeed;
}

// 重写useItem,并在其中调用父类的userItem方法
public void useItem(Item i) {
System.out.println("adhero use item");
super.useItem(i);
}

public static void main(String[] args) {
ADHero h = new ADHero();

LifePotion lp = new LifePotion();

}

}

Object类

Object类是所有类的父类

声明一个类的时候,默认是继承了Object
public class Hero extends Object

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
package charactor;

import property.Item;

public class Hero extends Object {

String name; //姓名

float hp; //血量

float armor; //护甲

int moveSpeed; //移动速度

public void useItem(Item i){
System.out.println("hero use item");
i.effect();
}

public Hero(){
System.out.println("Hero的无参的构造方法 ");
}

public Hero(String name){
System.out.println("Hero的有一个参数的构造方法 ");
this.name = name;
}

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

}

toString()

Object类提供一个toString方法,所以所有的类都有toString方法
toString()的意思是返回当前对象的字符串表达
通过 System.out.println 打印对象就是打印该对象的toString()返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package charactor;

public class Hero {
public String name;
protected float hp;

public String toString(){
return name;
}

public static void main(String[] args) {

Hero h = new Hero();
h.name = "盖伦";
System.out.println(h.toString());
//直接打印对象就是打印该对象的toString()返回值
System.out.println(h);
}
}

finalize()

当一个对象没有任何引用指向的时候,它就满足垃圾回收的条件
当它被垃圾回收的时候,它的finalize() 方法就会被调用。
finalize() 不是开发人员主动调用的方法,而是由虚拟机JVM调用的。

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
package charactor;

public class Hero {
public String name;
protected float hp;

public String toString(){
return name;
}

public void finalize(){
System.out.println("这个英雄正在被回收");
}

public static void main(String[] args) {
//只有一引用
Hero h;
for (int i = 0; i < 100000; i++) {
//不断生成新的对象
//每创建一个对象,前一个对象,就没有引用指向了
//那些对象,就满足垃圾回收的条件
//当,垃圾堆积的比较多的时候,就会触发垃圾回收
//一旦这个对象被回收,它的finalize()方法就会被调用
h = new Hero();
}

}
}

equals()

equals() 用于判断两个对象的内容是否相同
假设,当两个英雄的hp相同的时候,我们就认为这两个英雄相同。

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
package charactor;

public class Hero {
public String name;
protected float hp;

public boolean equals(Object o){
if(o instanceof Hero){
Hero h = (Hero) o;
return this.hp == h.hp;
}
return false;
}

public static void main(String[] args) {
Hero h1= new Hero();
h1.hp = 300;
Hero h2= new Hero();
h2.hp = 400;
Hero h3= new Hero();
h3.hp = 300;

System.out.println(h1.equals(h2));
System.out.println(h1.equals(h3));
}
}

==

这不是Object的方法,但是用于判断两个对象是否相同
更准确的讲,用于判断两个引用,是否指向了同一个对象

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
package charactor;

public class Hero {
public String name;
protected float hp;

public boolean equals(Object o){
if(o instanceof Hero){
Hero h = (Hero) o;
return this.hp == h.hp;
}
return false;
}

public static void main(String[] args) {
Hero h1= new Hero();
h1.hp = 300;
Hero h2= new Hero();
h2.hp = 400;
Hero h3= new Hero();
h3.hp = 300;

System.out.println(h1==h2);
System.out.println(h1==h3);

}
}

finial

final修饰类

当Hero被修饰成final的时候,表示Hero不能够被继承
其子类会出现编译错误

1
2
3
4
5
6
7
8
9
package charactor;

public final class Hero extends Object {

String name; //姓名

float hp; //血量

}

final修饰方法

Hero的useItem方法被修饰成final,那么该方法在ADHero中,不能够被重写

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
package charactor;

import property.Item;

public class Hero extends Object {

String name; //姓名

float hp; //血量

float armor; //护甲

int moveSpeed; //移动速度

public final void useItem(Item i){
System.out.println("hero use item");
i.effect();
}

public Hero(){
System.out.println("Hero的无参的构造方法 ");
}

public Hero(String name){
System.out.println("Hero的有一个参数的构造方法 ");
this.name = name;
}

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

}

final修饰基本类型变量

final修饰基本类型变量,表示该变量只有一次赋值机会
16行进行了赋值,17行就不可以再进行赋值了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package charactor;

public class Hero extends Object {

String name; //姓名

float hp; //血量

float armor; //护甲

int moveSpeed; //移动速度

public static void main(String[] args) {

final int hp;
hp = 5;
hp = 6;

}
}

抽象类

为Hero增加一个抽象方法 attack,并且把Hero声明为abstract的。
APHero, ADHero, ADAPHero是Hero的子类,继承了Hero的属性和方法。
但是各自的攻击手段是不一样的,所以继承Hero类后,这些子类就必须提供不一样的attack方法实现。

Hero.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package charactor;

public abstract class Hero {
String name;

float hp;

float armor;

int moveSpeed;

public static void main(String[] args) {

}

// 抽象方法attack
// Hero的子类会被要求实现attack方法
public abstract void attack();

}

ADHero.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package charactor;

public class ADHero extends Hero implements AD {

public void physicAttack() {
System.out.println("进行物理攻击");
}

@Override
public void attack() {
physicAttack();
}

}

APHero.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package charactor;

public class APHero extends Hero implements AP {

@Override
public void magicAttack() {
System.out.println("进行魔法攻击");
}

@Override
public void attack() {
magicAttack();
}

}

ADAPHero.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package charactor;

public class ADAPHero extends Hero implements AD, AP {

@Override
public void attack() {

System.out.println("既可以进行物理攻击,也可以进行魔法攻击");
}

public void magicAttack() {
System.out.println("进行魔法攻击");
}

public void physicAttack() {
System.out.println("进行物理攻击");
}

}

抽象类可以没有抽象方法

Hero类可以在不提供抽象方法的前提下,声明为抽象类
一旦一个类被声明为抽象类,就不能够被直接实例化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package charactor;

public abstract class Hero {
String name;

float hp;

float armor;

int moveSpeed;

public static void main(String[] args) {
//虽然没有抽象方法,但是一旦被声明为了抽象类,就不能够直接被实例化
Hero h= new Hero();
}

}

抽象类和接口的区别

区别1:

  • 子类只能继承一个抽象类,不能继承多个
  • 子类可以实现多个接口
    区别2:
    抽象类可以定义
  • public, protected, package, private
  • 静态和非静态属性
  • final和非final属性

但是接口中声明的属性,只能是:

  • public
  • 静态
  • final的

即便没有显式的声明

注:抽象类和接口都可以有实体方法。 接口中的实体方法,叫做默认方法

1
2
3
4
5
6
7
8
9
10
11
12
package charactor;

public interface AP {

public static final int resistPhysic = 100;

//resistMagic即便没有显式的声明为 public static final
//但依然默认为public static final
int resistMagic = 0;

public void magicAttack();
}

内部类

非静态内部类

非静态内部类 BattleScore “战斗成绩”
非静态内部类可以直接在一个类里面定义

比如:
战斗成绩只有在一个英雄对象存在的时候才有意义
所以实例化BattleScore 的时候,必须建立在一个存在的英雄的基础上
语法: new 外部类().new 内部类()
作为Hero的非静态内部类,是可以直接访问外部类的private实例属性name的。

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
package charactor;

public class Hero {
private String name; // 姓名

float hp; // 血量

float armor; // 护甲

int moveSpeed; // 移动速度

// 非静态内部类,只有一个外部类对象存在的时候,才有意义
// 战斗成绩只有在一个英雄对象存在的时候才有意义
class BattleScore {
int kill;
int die;
int assit;

public void legendary() {
if (kill >= 8)
System.out.println(name + "超神!");
else
System.out.println(name + "尚未超神!");
}
}

public static void main(String[] args) {
Hero garen = new Hero();
garen.name = "盖伦";
// 实例化内部类
// BattleScore对象只有在一个英雄对象存在的时候才有意义
// 所以其实例化必须建立在一个外部类对象的基础之上
BattleScore score = garen.new BattleScore();
score.kill = 9;
score.legendary();
}

}

静态内部类

在一个类里面声明一个静态内部类
比如敌方水晶,当敌方水晶没有血的时候,己方所有英雄都取得胜利,而不只是某一个具体的英雄取得胜利。
与非静态内部类不同,静态内部类水晶类的实例化 不需要一个外部类的实例为基础,可以直接实例化

语法:new 外部类.静态内部类();
因为没有一个外部类的实例,所以在静态内部类里面不可以访问外部类的实例属性和方法
除了可以访问外部类的私有静态成员外,静态内部类和普通类没什么大的区别。

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
package charactor;

public class Hero {
public String name;
protected float hp;

private static void battleWin(){
System.out.println("battle win");
}

//敌方的水晶
static class EnemyCrystal{
int hp=5000;

//如果水晶的血量为0,则宣布胜利
public void checkIfVictory(){
if(hp==0){
Hero.battleWin();

//静态内部类不能直接访问外部类的对象属性
System.out.println(name + " win this game");
}
}
}

public static void main(String[] args) {
//实例化静态内部类
Hero.EnemyCrystal crystal = new Hero.EnemyCrystal();
crystal.checkIfVictory();
}

}

匿名类

匿名类指的是在声明一个类的同时实例化它,使代码更加简洁精练
通常情况下,要使用一个接口或者抽象类,都必须创建一个子类

有的时候,为了快速使用,直接实例化一个抽象类,并“当场”实现其抽象方法。
既然实现了抽象方法,那么就是一个新的类,只是这个类,没有命名。
这样的类,叫做匿名类。

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
package charactor;

public abstract class Hero {
String name; //姓名

float hp; //血量

float armor; //护甲

int moveSpeed; //移动速度

public abstract void attack();

public static void main(String[] args) {

ADHero adh=new ADHero();
//通过打印adh,可以看到adh这个对象属于ADHero类
adh.attack();
System.out.println(adh);

Hero h = new Hero(){
//当场实现attack方法
public void attack() {
System.out.println("新的进攻手段");
}
};
h.attack();
//通过打印h,可以看到h这个对象属于Hero$1这么一个系统自动分配的类名

System.out.println(h);
}

}

本地类

本地类可以理解为有名字的匿名类
内部类与匿名类不一样的是,内部类必须声明在成员的位置,即与属性和方法平等的位置。
本地类和匿名类一样,直接声明在代码块里面,可以是主方法,for循环里等等地方。

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
package charactor;

public abstract class Hero {
String name; //姓名

float hp; //血量

float armor; //护甲

int moveSpeed; //移动速度

public abstract void attack();

public static void main(String[] args) {

//与匿名类的区别在于,本地类有了自定义的类名
class SomeHero extends Hero{
public void attack() {
System.out.println( name+ " 新的进攻手段");
}
}

SomeHero h =new SomeHero();
h.name ="地卜师";
h.attack();
}

}

在匿名类中使用外部的局部变量

在匿名类中使用外部的局部变量,外部的局部变量必须修饰为final

为什么要声明为final,其机制比较复杂,请参考第二个Hero代码中的解释

注:在jdk8中,已经不需要强制修饰成final了,如果没有写final,不会报错,因为编译器偷偷的帮你加上了看不见的final

Hero.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package charactor;

public abstract class Hero {

public abstract void attack();

public static void main(String[] args) {

//在匿名类中使用外部的局部变量,外部的局部变量必须修饰为final
final int damage = 5;

Hero h = new Hero(){
public void attack() {
System.out.printf("新的进攻手段,造成%d点伤害",damage );
}
};

}

}

Hero.java

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
package charactor;

public abstract class Hero {

public abstract void attack();

public static void main(String[] args) {

//在匿名类中使用外部的局部变量damage 必须修饰为final
int damage = 5;

//这里使用本地类AnonymousHero来模拟匿名类的隐藏属性机制

//事实上的匿名类,会在匿名类里声明一个damage属性,并且使用构造方法初始化该属性的值
//在attack中使用的damage,真正使用的是这个内部damage,而非外部damage

//假设外部属性不需要声明为final
//那么在attack中修改damage的值,就会被暗示为修改了外部变量damage的值

//但是他们俩是不同的变量,是不可能修改外部变量damage的
//所以为了避免产生误导,外部的damage必须声明为final,"看上去"就不能修改了
class AnonymousHero extends Hero{
int damage;
public AnonymousHero(int damage){
this.damage = damage;
}
public void attack() {
damage = 10;
System.out.printf("新的进攻手段,造成%d点伤害",this.damage );
}
}

Hero h = new AnonymousHero(damage);

}

}

默认方法

什么是默认方法

默认方法是JDK8新特性,指的是接口也可以提供具体方法了,而不像以前,只能提供抽象方法。

Mortal 这个接口,增加了一个默认方法 revive,这个方法有实现体,并且被声明为了default

1
2
3
4
5
6
7
8
9
package charactor;

public interface Mortal {
public void die();

default public void revive() {
System.out.println("本英雄复活了");
}
}

为什么会有默认方法

假设没有默认方法这种机制,那么如果要为Mortal增加一个新的方法revive,那么所有实现了Mortal接口的类,都需要做改动。

但是引入了默认方法后,原来的类,不需要做任何改动,并且还能得到这个默认方法。

通过这种手段,就能够很好的扩展新的类,并且做到不影响原来的类。

数字与字符串

装箱拆箱

封装类

所有的基本类型,都有对应的类类型
比如int对应的类是Integer
这种类就叫做封装类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package digit;

public class TestNumber {

public static void main(String[] args) {
int i = 5;

//把一个基本类型的变量,转换为Integer对象
Integer it = new Integer(i);
//把一个Integer对象,转换为一个基本类型的int
int i2 = it.intValue();

}
}

Number类

数字封装类有
Byte,Short,Integer,Long,Float,Double
这些类都是抽象类Number的子类

1
2
3
4
5
6
7
8
9
10
11
12
package digit;

public class TestNumber {

public static void main(String[] args) {
int i = 5;

Integer it = new Integer(i);
//Integer是Number的子类,所以打印true
System.out.println(it instanceof Number);
}
}

基本类型转封装类

1
2
3
4
5
6
7
8
9
10
11
12
package digit;

public class TestNumber {

public static void main(String[] args) {
int i = 5;

//基本类型转换成封装类型
Integer it = new Integer(i);

}
}

封装类转基本类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package digit;

public class TestNumber {

public static void main(String[] args) {
int i = 5;

//基本类型转换成封装类型
Integer it = new Integer(i);

//封装类型转换成基本类型
int i2 = it.intValue();

}
}

自动装箱

不需要调用构造方法,通过=符号自动把 基本类型 转换为 类类型 就叫装箱。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package digit;

public class TestNumber {

public static void main(String[] args) {
int i = 5;

//基本类型转换成封装类型
Integer it = new Integer(i);

//自动转换就叫装箱
Integer it2 = i;

}
}

自动拆箱

不需要调用Integer的intValue方法,通过=就自动转换成int类型,就叫拆箱。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package digit;

public class TestNumber {

public static void main(String[] args) {
int i = 5;

Integer it = new Integer(i);

//封装类型转换成基本类型
int i2 = it.intValue();

//自动转换就叫拆箱
int i3 = it;

}
}

int的最大值,最小值

int的最大值可以通过其对应的封装类Integer.MAX_VALUE获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
package digit;

public class TestNumber {

public static void main(String[] args) {

//int的最大值
System.out.println(Integer.MAX_VALUE);
//int的最小值
System.out.println(Integer.MIN_VALUE);

}
}

字符串转换

数字转字符串

  • 方法1: 使用String类的静态方法valueOf
  • 方法2: 先把基本类型装箱为对象,然后调用对象的toString
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package digit;

public class TestNumber {

public static void main(String[] args) {
int i = 5;

//方法1
String str = String.valueOf(i);

//方法2
Integer it = i;
String str2 = it.toString();

}
}

字符串转数字

调用Integer的静态方法parseInt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package digit;

public class TestNumber {

public static void main(String[] args) {

String str = "999";

int i= Integer.parseInt(str);

System.out.println(i);

}
}

数学方法

四舍五入, 随机数,开方,次方,π,自然常数

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
package digit;

public class TestNumber {

public static void main(String[] args) {
float f1 = 5.4f;
float f2 = 5.5f;
//5.4四舍五入即5
System.out.println(Math.round(f1));
//5.5四舍五入即6
System.out.println(Math.round(f2));

//得到一个0-1之间的随机浮点数(取不到1)
System.out.println(Math.random());

//得到一个0-10之间的随机整数 (取不到10)
System.out.println((int)( Math.random()*10));
//开方
System.out.println(Math.sqrt(9));
//次方(2的4次方)
System.out.println(Math.pow(2,4));

//π
System.out.println(Math.PI);

//自然常数
System.out.println(Math.E);
}
}

格式化输出

如果不使用格式化输出,就需要进行字符串连接,如果变量比较多,拼接就会显得繁琐。使用格式化输出,就可以简洁明了。

%s 表示字符串
%d 表示数字
%n 表示换行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package digit;

public class TestNumber {

public static void main(String[] args) {

String name ="盖伦";
int kill = 8;
String title="超神";

//直接使用+进行字符串连接,编码感觉会比较繁琐,并且维护性差,易读性差
String sentence = name+ " 在进行了连续 " + kill + " 次击杀后,获得了 " + title +" 的称号";

System.out.println(sentence);

//使用格式化输出
//%s表示字符串,%d表示数字,%n表示换行
String sentenceFormat ="%s 在进行了连续 %d 次击杀后,获得了 %s 的称号%n";
System.out.printf(sentenceFormat,name,kill,title);

}
}

printf 和 format

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package digit;

public class TestNumber {

public static void main(String[] args) {

String name ="盖伦";
int kill = 8;
String title="超神";

String sentenceFormat ="%s 在进行了连续 %d 次击杀后,获得了 %s 的称号%n";
//使用printf格式化输出
System.out.printf(sentenceFormat,name,kill,title);
//使用format格式化输出
System.out.format(sentenceFormat,name,kill,title);

}
}

换行符

换行符就是另起一行 — ‘\n’ 换行(newline)
回车符就是回到一行的开头 — ‘\r’ 回车(return)
在eclipse里敲一个回车,实际上是回车换行符
Java是跨平台的编程语言,同样的代码,可以在不同的平台使用,比如Windows, Linux, Mac

然而在不同的操作系统,换行符是不一样的
(1)在DOS和Windows中,每行结尾是 “\r\n”;
(2)Linux系统里,每行结尾只有 “\n”;
(3)Mac系统里,每行结尾是只有 “\r”。

为了使得同一个java程序的换行符在所有的操作系统中都有一样的表现,使用%n,就可以做到平台无关的换行

1
2
3
4
5
6
7
8
9
10
11
package digit;

public class TestNumber {

public static void main(String[] args) {

System.out.printf("这是换行符%n");
System.out.printf("这是换行符%n");

}
}

总长度,左对齐,补0,千位分隔符,小数点位数,本地化表达

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
package digit;

import java.util.Locale;

public class TestNumber {

public static void main(String[] args) {
int year = 2020;
//总长度,左对齐,补0,千位分隔符,小数点位数,本地化表达

//直接打印数字
System.out.format("%d%n",year);
//总长度是8,默认右对齐
System.out.format("%8d%n",year);
//总长度是8,左对齐
System.out.format("%-8d%n",year);
//总长度是8,不够补0
System.out.format("%08d%n",year);
//千位分隔符
System.out.format("%,8d%n",year*10000);

//小数点位数
System.out.format("%.2f%n",Math.PI);

//不同国家的千位分隔符
System.out.format(Locale.FRANCE,"%,.2f%n",Math.PI*10000);
System.out.format(Locale.US,"%,.2f%n",Math.PI*10000);
System.out.format(Locale.UK,"%,.2f%n",Math.PI*10000);

}
}

字符

保存一个字符的时候使用char

1
2
3
4
5
6
7
8
9
10
11
12
package character;

public class TestChar {

public static void main(String[] args) {
char c1 = 'a';
char c2 = '1';//字符1,而非数字1
char c3 = '中';//汉字字符
char c4 = 'ab'; //只能放一个字符

}
}

char对应的封装类

char对应的封装类是Character

1
2
3
4
5
6
7
8
9
10
11
package character;

public class TestChar {

public static void main(String[] args) {
char c1 = 'a';
Character c = c1; //自动装箱
c1 = c;//自动拆箱

}
}

Character常见方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package character;

public class TestChar {

public static void main(String[] args) {

System.out.println(Character.isLetter('a'));//判断是否为字母
System.out.println(Character.isDigit('a')); //判断是否为数字
System.out.println(Character.isWhitespace(' ')); //是否是空白
System.out.println(Character.isUpperCase('a')); //是否是大写
System.out.println(Character.isLowerCase('a')); //是否是小写

System.out.println(Character.toUpperCase('a')); //转换为大写
System.out.println(Character.toLowerCase('A')); //转换为小写

String a = 'a'; //不能够直接把一个字符转换成字符串
String a2 = Character.toString('a'); //转换为字符串

}
}

常见转义

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
package character;

public class TestChar {

public static void main(String[] args) {
System.out.println("使用空格无法达到对齐的效果");
System.out.println("abc def");
System.out.println("ab def");
System.out.println("a def");

System.out.println("使用\\t制表符可以达到对齐的效果");
System.out.println("abc\tdef");
System.out.println("ab\tdef");
System.out.println("a\tdef");

System.out.println("一个\\t制表符长度是8");
System.out.println("12345678def");

System.out.println("换行符 \\n");
System.out.println("abc\ndef");

System.out.println("单引号 \\'");
System.out.println("abc\'def");
System.out.println("双引号 \\\"");
System.out.println("abc\"def");
System.out.println("反斜杠本身 \\");
System.out.println("abc\\def");
}
}

字符串

创建字符串

字符串即字符的组合,在Java中,字符串是一个类,所以我们见到的字符串都是对象
常见创建字符串手段:

  1. 每当有一个字面值出现的时候,虚拟机就会创建一个字符串
  2. 调用String的构造方法创建一个字符串对象
  3. 通过+加号进行字符串拼接也会创建新的字符串对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package character;

public class TestString {

public static void main(String[] args) {
String garen ="盖伦"; //字面值,虚拟机碰到字面值就会创建一个字符串对象

String teemo = new String("提莫"); //创建了两个字符串对象

char[] cs = new char[]{'崔','斯','特'};

String hero = new String(cs);// 通过字符数组创建一个字符串对象

String hero3 = garen + teemo;// 通过+加号进行字符串拼接
}
}

final

String 被修饰为final,所以是不能被继承的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package character;

public class TestString {

public static void main(String[] args) {
MyString str = new MyString();

}

/*这里会报错,因为String不能被继承*/
static class MyString extends String{

}

}

immutable

immutable 是指不可改变的
比如创建了一个字符串对象
String garen =”盖伦”;

不可改变的具体含义是指:

  • 不能增加长度
  • 不能减少长度
  • 不能插入字符
  • 不能删除字符
  • 不能修改字符

一旦创建好这个字符串,里面的内容 永远 不能改变

String 的表现就像是一个常量

1
2
3
4
5
6
7
8
9
package character;

public class TestString {

public static void main(String[] args) {
String garen ="盖伦";

}
}

字符串格式化

如果不使用字符串格式化,就需要进行字符串连接,如果变量比较多,拼接就会显得繁琐
使用字符串格式化,就可以简洁明了。

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
package character;

public class TestString {

public static void main(String[] args) {

String name ="盖伦";
int kill = 8;
String title="超神";

//直接使用+进行字符串连接,编码感觉会比较繁琐,并且维护性差,易读性差
String sentence = name+ " 在进行了连续 " + kill + " 次击杀后,获得了 " + title +" 的称号";

System.out.println(sentence);

//格式化字符串
//%s表示字符串,%d表示数字,%n表示换行
String sentenceFormat ="%s 在进行了连续 %d 次击杀后,获得了 %s 的称号%n";

String sentence2 = String.format(sentenceFormat, name,kill,title);

System.out.println(sentence2);

}
}

字符串长度

length方法返回当前字符串的长度
可以有长度为0的字符串,即空字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package character;

public class TestString {

public static void main(String[] args) {

String name ="盖伦";

System.out.println(name.length());

String unknowHero = "";

//可以有长度为0的字符串,即空字符串
System.out.println(unknowHero.length());

}
}

操纵字符串

获取字符

charAt(int index)获取指定位置的字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package character;

public class TestString {

public static void main(String[] args) {

String sentence = "盖伦,在进行了连续8次击杀后,获得了 超神 的称号";

char c = sentence.charAt(0);

System.out.println(c);

}
}

获取对应的字符数组

toCharArray()
获取对应的字符数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package character;

public class TestString {

public static void main(String[] args) {

String sentence = "盖伦,在进行了连续8次击杀后,获得了超神 的称号";

char[] cs = sentence.toCharArray(); //获取对应的字符数组

System.out.println(sentence.length() == cs.length);

}
}

截取子字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package character;

public class TestString {

public static void main(String[] args) {

String sentence = "盖伦,在进行了连续8次击杀后,获得了 超神 的称号";

//截取从第3个开始的字符串 (基0)
String subString1 = sentence.substring(3);

System.out.println(subString1);

//截取从第3个开始的字符串 (基0)
//到5-1的位置的字符串
//左闭右开
String subString2 = sentence.substring(3,5);

System.out.println(subString2);

}
}

分隔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package character;

public class TestString {

public static void main(String[] args) {

String sentence = "盖伦,在进行了连续8次击杀后,获得了 超神 的称号";

//根据,进行分割,得到3个子字符串
String subSentences[] = sentence.split(",");
for (String sub : subSentences) {
System.out.println(sub);
}

}
}

去掉首尾空格

1
2
3
4
5
6
7
8
9
10
11
12
13
package character;

public class TestString {

public static void main(String[] args) {

String sentence = " 盖伦,在进行了连续8次击杀后,获得了 超神 的称号 ";

System.out.println(sentence);
//去掉首尾空格
System.out.println(sentence.trim());
}
}

大小写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package character;

public class TestString {

public static void main(String[] args) {

String sentence = "Garen";

//全部变成小写
System.out.println(sentence.toLowerCase());
//全部变成大写
System.out.println(sentence.toUpperCase());

}
}

定位

indexOf 判断字符或者子字符串出现的位置
contains 是否包含子字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package character;

public class TestString {

public static void main(String[] args) {

String sentence = "盖伦,在进行了连续8次击杀后,获得了超神 的称号";

System.out.println(sentence.indexOf('8')); //字符第一次出现的位置

System.out.println(sentence.indexOf("超神")); //字符串第一次出现的位置

System.out.println(sentence.lastIndexOf("了")); //字符串最后出现的位置

System.out.println(sentence.indexOf(',',5)); //从位置5开始,出现的第一次,的位置

System.out.println(sentence.contains("击杀")); //是否包含字符串"击杀"

}
}

替换

replaceAll 替换所有的
replaceFirst 只替换第一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package character;

public class TestString {

public static void main(String[] args) {

String sentence = "盖伦,在进行了连续8次击杀后,获得了超神 的称号";

String temp = sentence.replaceAll("击杀", "被击杀"); //替换所有的

temp = temp.replaceAll("超神", "超鬼");

System.out.println(temp);

temp = sentence.replaceFirst(",","");//只替换第一个

System.out.println(temp);

}
}

比较字符串

是否是同一个对象

str1和str2的内容一定是一样的!
但是,并不是同一个字符串对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package character;

public class TestString {

public static void main(String[] args) {

String str1 = "the light";

String str2 = new String(str1);

//==用于判断是否是同一个字符串对象
System.out.println( str1 == str2);

}

}

是否是同一个对象-特例

str1 = “the light”;
str3 = “the light”;

一般说来,编译器每碰到一个字符串的字面值,就会创建一个新的对象
所以在第6行会创建了一个新的字符串”the light”
但是在第7行,编译器发现已经存在现成的”the light”,那么就直接拿来使用,而没有进行重复创建。

1
2
3
4
5
6
7
8
9
10
11
package character;

public class TestString {

public static void main(String[] args) {
String str1 = "the light";
String str3 = "the light";
System.out.println( str1 == str3);
}

}

内容是否相同

使用equals进行字符串内容的比较,必须大小写一致
equalsIgnoreCase,忽略大小写判断内容是否一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package character;

public class TestString {

public static void main(String[] args) {

String str1 = "the light";

String str2 = new String(str1);

String str3 = str1.toUpperCase();

//==用于判断是否是同一个字符串对象
System.out.println( str1 == str2);

System.out.println(str1.equals(str2));//完全一样返回true

System.out.println(str1.equals(str3));//大小写不一样,返回false
System.out.println(str1.equalsIgnoreCase(str3));//忽略大小写的比较,返回true

}

}

是否以子字符串开始或者结束

startsWith //以…开始
endsWith //以…结束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package character;

public class TestString {

public static void main(String[] args) {
String str1 = "the light";

String start = "the";
String end = "Ight";

System.out.println(str1.startsWith(start));//以...开始
System.out.println(str1.endsWith(end));//以...结束

}

}

StringBuffer

追加 删除 插入 反转

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
package character;

public class TestString {

public static void main(String[] args) {
String str1 = "let there ";

StringBuffer sb = new StringBuffer(str1); //根据str1创建一个StringBuffer对象
sb.append("be light"); //在最后追加

System.out.println(sb);

sb.delete(4, 10);//删除4-10之间的字符

System.out.println(sb);

sb.insert(4, "there ");//在4这个位置插入 there

System.out.println(sb);

sb.reverse(); //反转

System.out.println(sb);

}

}

长度 容量

为什么StringBuffer可以变长?
和String内部是一个字符数组一样,StringBuffer也维护了一个字符数组。 但是,这个字符数组,留有冗余长度
比如说new StringBuffer(“the”),其内部的字符数组的长度,是19,而不是3,这样调用插入和追加,在现成的数组的基础上就可以完成了。
如果追加的长度超过了19,就会分配一个新的数组,长度比原来多一些,把原来的数据复制到新的数组中,看上去 数组长度就变长了。
length: “the”的长度 3
capacity: 分配的总空间 19

注: 19这个数量,不同的JDK数量是不一样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package character;

public class TestString {

public static void main(String[] args) {
String str1 = "the";

StringBuffer sb = new StringBuffer(str1);

System.out.println(sb.length()); //内容长度

System.out.println(sb.capacity());//总空间

}

}

日期

Date

Date类
注意:是java.util.Date;
而非 java.sql.Date,此类是给数据库访问的时候使用的

时间原点概念

所有的数据类型,无论是整数,布尔,浮点数还是字符串,最后都需要以数字的形式表现出来。

日期类型也不例外,换句话说,一个日期,比如2020年10月1日,在计算机里,会用一个数字来代替。

那么最特殊的一个数字,就是零. 零这个数字,就代表Java中的时间原点,其对应的日期是1970年1月1日 8点0分0秒 。 (为什么是8点,因为中国的太平洋时区是UTC-8,刚好和格林威治时间差8个小时)

为什么对应1970年呢? 因为1969年发布了第一个 UNIX 版本:AT&T,综合考虑,当时就把1970年当做了时间原点。

所有的日期,都是以为这个0点为基准,每过一毫秒,就+1。

创建日期对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package date;

//
import java.util.Date;

public class TestDate {

public static void main(String[] args) {

// 当前时间
Date d1 = new Date();
System.out.println("当前时间:");
System.out.println(d1);
System.out.println();
// 从1970年1月1日 早上8点0分0秒 开始经历的毫秒数
Date d2 = new Date(5000);
System.out.println("从1970年1月1日 早上8点0分0秒 开始经历了5秒的时间");
System.out.println(d2);

}
}

getTime

getTime() 得到一个long型的整数
这个整数代表 从1970.1.1 08:00:00:000 开始 每经历一毫秒,增加1
直接打印对象,会看到 “Tue Jan 05 09:51:48 CST 2016” 这样的格式,可读性比较差,为了获得“2016/1/5 09:51:48”这样的格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package date;

//
import java.util.Date;

public class TestDate {

public static void main(String[] args) {
//注意:是java.util.Date;
//而非 java.sql.Date,此类是给数据库访问的时候使用的
Date now= new Date();
//打印当前时间
System.out.println("当前时间:"+now.toString());
//getTime() 得到一个long型的整数
//这个整数代表 1970.1.1 08:00:00:000,每经历一毫秒,增加1
System.out.println("当前时间getTime()返回的值是:"+now.getTime());

Date zero = new Date(0);
System.out.println("用0作为构造方法,得到的日期是:"+zero);

}
}

System.currentTimeMillis()

当前日期的毫秒数
new Date().getTime() 和 System.currentTimeMillis() 是一样的
不过由于机器性能的原因,可能会相差几十毫秒,毕竟每执行一行代码,都是需要时间的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package date;

//
import java.util.Date;

public class TestDate {

public static void main(String[] args) {
Date now= new Date();

//当前日期的毫秒数
System.out.println("Date.getTime() \t\t\t返回值: "+now.getTime());
//通过System.currentTimeMillis()获取当前日期的毫秒数
System.out.println("System.currentTimeMillis() \t返回值: "+System.currentTimeMillis());

}
}

日期格式化

日期转字符串

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
package date;

//
import java.text.SimpleDateFormat;
import java.util.Date;

public class TestDate {

public static void main(String[] args) {

//y 代表年
//M 代表月
//d 代表日
//H 代表24进制的小时
//h 代表12进制的小时
//m 代表分钟
//s 代表秒
//S 代表毫秒
SimpleDateFormat sdf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS" );
Date d= new Date();
String str = sdf.format(d);
System.out.println("当前时间通过 yyyy-MM-dd HH:mm:ss SSS 格式化后的输出: "+str);

SimpleDateFormat sdf1 =new SimpleDateFormat("yyyy-MM-dd" );
Date d1= new Date();
String str1 = sdf1.format(d1);
System.out.println("当前时间通过 yyyy-MM-dd 格式化后的输出: "+str1);

}
}

字符串转日期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package date;

//
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class TestDate {

public static void main(String[] args) {
SimpleDateFormat sdf =new SimpleDateFormat("yyyy/MM/dd HH:mm:ss" );

String str = "2016/1/5 12:12:12";

try {
Date d = sdf.parse(str);
System.out.printf("字符串 %s 通过格式 yyyy/MM/dd HH:mm:ss %n转换为日期对象: %s",str,d.toString());
} catch (ParseException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}

Calendar

Calendar与Date进行转换

采用单例模式获取日历对象Calendar.getInstance();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package date;

//
import java.util.Calendar;
import java.util.Date;

public class TestDate {

public static void main(String[] args) {
//采用单例模式获取日历对象Calendar.getInstance();
Calendar c = Calendar.getInstance();

//通过日历对象得到日期对象
Date d = c.getTime();

Date d2 = new Date(0);
c.setTime(d2); //把这个日历,调成日期 : 1970.1.1 08:00:00
}
}

翻日历

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
package date;

import java.text.SimpleDateFormat;
//
import java.util.Calendar;
import java.util.Date;

public class TestDate {

private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

public static void main(String[] args) {
Calendar c = Calendar.getInstance();
Date now = c.getTime();
// 当前日期
System.out.println("当前日期:\t" + format(c.getTime()));

// 下个月的今天
c.setTime(now);
c.add(Calendar.MONTH, 1);
System.out.println("下个月的今天:\t" +format(c.getTime()));

// 去年的今天
c.setTime(now);
c.add(Calendar.YEAR, -1);
System.out.println("去年的今天:\t" +format(c.getTime()));

// 上个月的第三天
c.setTime(now);
c.add(Calendar.MONTH, -1);
c.set(Calendar.DATE, 3);
System.out.println("上个月的第三天:\t" +format(c.getTime()));

}

private static String format(Date time) {
return sdf.format(time);
}
}

异常处理

什么是异常

文件不存在异常

比如要打开d盘的LOL.exe文件,这个文件是有可能不存在的
Java中通过 new FileInputStream(f) 试图打开某文件,就有可能抛出文件不存在异常FileNotFoundException
如果不处理该异常,就会有编译错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package exception;

import java.io.File;
import java.io.FileInputStream;

public class TestException {

public static void main(String[] args) {

File f= new File("d:/LOL.exe");

//试图打开文件LOL.exe,会抛出FileNotFoundException,如果不处理该异常,就会有编译错误
new FileInputStream(f);

}
}

异常处理

try catch

  1. 将可能抛出FileNotFoundException 文件不存在异常的代码放在try里
  2. 如果文件存在,就会顺序往下执行,并且不执行catch块中的代码
  3. 如果文件不存在,try 里的代码会立即终止,程序流程会运行到对应的catch块中
  4. e.printStackTrace(); 会打印出方法的调用痕迹,如此例,会打印出异常开始于TestException的第16行,这样就便于定位和分析到底哪里出了异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package exception;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;

public class TestException {

public static void main(String[] args) {

File f= new File("d:/LOL.exe");

try{
System.out.println("试图打开 d:/LOL.exe");
new FileInputStream(f);
System.out.println("成功打开");
}
catch(FileNotFoundException e){
System.out.println("d:/LOL.exe不存在");
e.printStackTrace();
}

}
}

使用异常的父类进行catch

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
package exception;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;

public class TestException {

public static void main(String[] args) {

File f= new File("d:/LOL.exe");

try{
System.out.println("试图打开 d:/LOL.exe");
new FileInputStream(f);
System.out.println("成功打开");
}

catch(Exception e){
System.out.println("d:/LOL.exe不存在");
e.printStackTrace();
}

}
}

多异常捕捉办法1

有的时候一段代码会抛出多种异常,比如

new FileInputStream(f); Date d = sdf.parse("2016-06-03");

这段代码,会抛出 文件不存在异常 FileNotFoundException 和 解析异常ParseException
解决办法之一是分别进行catch

1
2
3
4
5
6
7
catch (FileNotFoundException e) {
System.out.println("d:/LOL.exe不存在");
e.printStackTrace();
} catch (ParseException e) {
System.out.println("日期格式解析错误");
e.printStackTrace();
}
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
package exception;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class TestException {

public static void main(String[] args) {

File f = new File("d:/LOL.exe");

try {
System.out.println("试图打开 d:/LOL.exe");
new FileInputStream(f);
System.out.println("成功打开");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date d = sdf.parse("2016-06-03");
} catch (FileNotFoundException e) {
System.out.println("d:/LOL.exe不存在");
e.printStackTrace();
} catch (ParseException e) {
System.out.println("日期格式解析错误");
e.printStackTrace();
}
}
}

多异常捕捉办法2

另一个种办法是把多个异常,放在一个catch里统一捕捉

catch (FileNotFoundException | ParseException e) {

这种方式从 JDK7开始支持,好处是捕捉的代码更紧凑,不足之处是,一旦发生异常,不能确定到底是哪种异常,需要通过instanceof 进行判断具体的异常类型

if (e instanceof FileNotFoundException) System.out.println("d:/LOL.exe不存在"); if (e instanceof ParseException) System.out.println("日期格式解析错误");

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
package exception;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class TestException {

public static void main(String[] args) {

File f = new File("d:/LOL.exe");

try {
System.out.println("试图打开 d:/LOL.exe");
new FileInputStream(f);
System.out.println("成功打开");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date d = sdf.parse("2016-06-03");
} catch (FileNotFoundException | ParseException e) {
if (e instanceof FileNotFoundException)
System.out.println("d:/LOL.exe不存在");
if (e instanceof ParseException)
System.out.println("日期格式解析错误");

e.printStackTrace();
}

}
}

finally

无论是否出现异常,finally中的代码都会被执行

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
package exception;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;

public class TestException {

public static void main(String[] args) {

File f= new File("d:/LOL.exe");

try{
System.out.println("试图打开 d:/LOL.exe");
new FileInputStream(f);
System.out.println("成功打开");
}
catch(FileNotFoundException e){
System.out.println("d:/LOL.exe不存在");
e.printStackTrace();
}
finally{
System.out.println("无论文件是否存在, 都会执行的代码");
}
}
}

throws

考虑如下情况:
主方法调用method1
method1调用method2
method2中打开文件

method2中需要进行异常处理
但是method2不打算处理,而是把这个异常通过throws抛出去
那么method1就会接到该异常。 处理办法也是两种,要么是try catch处理掉,要么也是抛出去
method1选择本地try catch住 一旦try catch住了,就相当于把这个异常消化掉了,主方法在调用method1的时候,就不需要进行异常处理了

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
package exception;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;

public class TestException {

public static void main(String[] args) {
method1();

}

private static void method1() {
try {
method2();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}

private static void method2() throws FileNotFoundException {

File f = new File("d:/LOL.exe");

System.out.println("试图打开 d:/LOL.exe");
new FileInputStream(f);
System.out.println("成功打开");

}
}

throw和throws的区别

throws与throw这两个关键字接近,不过意义不一样,有如下区别:

  1. throws 出现在方法声明上,而throw通常都出现在方法体内。
  2. throws 表示出现异常的一种可能性,并不一定会发生这些异常;throw则是抛出了异常,执行throw则一定抛出了某个异常对象。

异常分类

可查异常

可查异常: CheckedException
可查异常即必须进行处理的异常,要么try catch住,要么往外抛,谁调用,谁处理,比如 FileNotFoundException
如果不处理,编译器,就不让你通过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package exception;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;

public class TestException {

public static void main(String[] args) {

File f= new File("d:/LOL.exe");

try{
System.out.println("试图打开 d:/LOL.exe");
new FileInputStream(f);
System.out.println("成功打开");
}
catch(FileNotFoundException e){
System.out.println("d:/LOL.exe不存在");
e.printStackTrace();
}

}
}

运行时异常

运行时异常RuntimeException指: 不是必须进行try catch的异常

常见运行时异常:

  • 除数不能为0异常:ArithmeticException
  • 下标越界异常:ArrayIndexOutOfBoundsException
  • 空指针异常:NullPointerException

在编写代码的时候,依然可以使用try catch throws进行处理,与可查异常不同之处在于,即便不进行try catch,也不会有编译错误
Java之所以会设计运行时异常的原因之一,是因为下标越界,空指针这些运行时异常太过于普遍,如果都需要进行捕捉,代码的可读性就会变得很糟糕。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package exception;

public class TestException {

public static void main(String[] args) {

//任何除数不能为0:ArithmeticException
int k = 5/0;

//下标越界异常:ArrayIndexOutOfBoundsException
int j[] = new int[5];
j[10] = 10;

//空指针异常:NullPointerException
String str = null;
str.length();
}
}

错误

错误Error,指的是系统级别的异常,通常是内存用光了
默认设置下,一般java程序启动的时候,最大可以使用16m的内存
如例不停的给StringBuffer追加字符,很快就把内存使用光了。抛出OutOfMemoryError
与运行时异常一样,错误也是不要求强制捕捉的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package exception;

public class TestException {

public static void main(String[] args) {

StringBuffer sb =new StringBuffer();

for (int i = 0; i < Integer.MAX_VALUE; i++) {
sb.append('a');
}

}

}

三种分类

总体上异常分三类:

  1. 错误
  2. 运行时异常
  3. 可查异常

Throwable

Throwable是类,Exception和Error都继承了该类
所以在捕捉的时候,也可以使用Throwable进行捕捉
如图: 异常分ErrorException
Exception里又分运行时异常可查异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package exception;

import java.io.File;
import java.io.FileInputStream;

public class TestException {

public static void main(String[] args) {

File f = new File("d:/LOL.exe");

try {
new FileInputStream(f);
//使用Throwable进行异常捕捉
} catch (Throwable t) {
// TODO Auto-generated catch block
t.printStackTrace();
}

}
}

自定义异常

创建自定义异常

一个英雄攻击另一个英雄的时候,如果发现另一个英雄已经挂了,就会抛出EnemyHeroIsDeadException
创建一个类EnemyHeroIsDeadException,并继承Exception
提供两个构造方法

  1. 无参的构造方法
  2. 带参的构造方法,并调用父类的对应的构造方法
1
2
3
4
5
6
7
8
9
class EnemyHeroIsDeadException extends Exception{

public EnemyHeroIsDeadException(){

}
public EnemyHeroIsDeadException(String msg){
super(msg);
}
}

抛出自定义异常

在Hero的attack方法中,当发现敌方英雄的血量为0的时候,抛出该异常

  1. 创建一个EnemyHeroIsDeadException实例
  2. 通过throw抛出该异常
  3. 当前方法通过throws抛出该异常

在外部调用attack方法的时候,就需要进行捕捉,并且捕捉的时候,可以通过e.getMessage() 获取当时出错的具体原因。

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
package charactor;

public class Hero {
public String name;
protected float hp;

public void attackHero(Hero h) throws EnemyHeroIsDeadException{
if(h.hp == 0){
throw new EnemyHeroIsDeadException(h.name + " 已经挂了,不需要施放技能" );
}
}

public String toString(){
return name;
}

class EnemyHeroIsDeadException extends Exception{

public EnemyHeroIsDeadException(){

}
public EnemyHeroIsDeadException(String msg){
super(msg);
}
}

public static void main(String[] args) {

Hero garen = new Hero();
garen.name = "盖伦";
garen.hp = 616;

Hero teemo = new Hero();
teemo.name = "提莫";
teemo.hp = 0;

try {
garen.attackHero(teemo);

} catch (EnemyHeroIsDeadException e) {
// TODO Auto-generated catch block
System.out.println("异常的具体原因:"+e.getMessage());
e.printStackTrace();
}

}
}

I/O

文件对象

创建一个文件对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package file;

import java.io.File;

public class TestFile {

public static void main(String[] args) {
// 绝对路径
File f1 = new File("d:/LOLFolder");
System.out.println("f1的绝对路径:" + f1.getAbsolutePath());
// 相对路径,相对于工作目录,如果在eclipse中,就是项目目录
File f2 = new File("LOL.exe");
System.out.println("f2的绝对路径:" + f2.getAbsolutePath());

// 把f1作为父目录创建文件对象
File f3 = new File(f1, "LOL.exe");

System.out.println("f3的绝对路径:" + f3.getAbsolutePath());
}
}

文件常用方法

注意1: 需要在D:\LOLFolder确实存在一个LOL.exe,才可以看到对应的文件长度、修改时间等信息

注意2: renameTo方法用于对物理文件名称进行修改,但是并不会修改File对象的name属性。

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
package file;

import java.io.File;
import java.util.Date;

public class TestFile {

public static void main(String[] args) {

File f = new File("d:/LOLFolder/LOL.exe");
System.out.println("当前文件是:" +f);
//文件是否存在
System.out.println("判断是否存在:"+f.exists());

//是否是文件夹
System.out.println("判断是否是文件夹:"+f.isDirectory());

//是否是文件(非文件夹)
System.out.println("判断是否是文件:"+f.isFile());

//文件长度
System.out.println("获取文件的长度:"+f.length());

//文件最后修改时间
long time = f.lastModified();
Date d = new Date(time);
System.out.println("获取文件的最后修改时间:"+d);
//设置文件修改时间为1970.1.1 08:00:00
f.setLastModified(0);

//文件重命名
File f2 =new File("d:/LOLFolder/DOTA.exe");
f.renameTo(f2);
System.out.println("把LOL.exe改名成了DOTA.exe");

System.out.println("注意: 需要在D:\\LOLFolder确实存在一个LOL.exe,\r\n才可以看到对应的文件长度、修改时间等信息");
}
}
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
package file;

import java.io.File;
import java.io.IOException;

public class TestFile {

public static void main(String[] args) throws IOException {

File f = new File("d:/LOLFolder/skin/garen.ski");

// 以字符串数组的形式,返回当前文件夹下的所有文件(不包含子文件及子文件夹)
f.list();

// 以文件数组的形式,返回当前文件夹下的所有文件(不包含子文件及子文件夹)
File[]fs= f.listFiles();

// 以字符串形式返回获取所在文件夹
f.getParent();

// 以文件形式返回获取所在文件夹
f.getParentFile();
// 创建文件夹,如果父文件夹skin不存在,创建就无效
f.mkdir();

// 创建文件夹,如果父文件夹skin不存在,就会创建父文件夹
f.mkdirs();

// 创建一个空文件,如果父文件夹skin不存在,就会抛出异常
f.createNewFile();
// 所以创建一个空文件之前,通常都会创建父目录
f.getParentFile().mkdirs();

// 列出所有的盘符c: d: e: 等等
f.listRoots();

// 刪除文件
f.delete();

// JVM结束的时候,刪除文件,常用于临时文件的删除
f.deleteOnExit();

}
}

什么是流

当不同的介质之间有数据交互的时候,JAVA就使用流来实现。
数据源可以是文件,还可以是数据库,网络甚至是其他的程序

比如读取文件的数据到程序中,站在程序的角度来看,就叫做输入流
输入流: InputStream
输出流:OutputStream

文件输入流

如下代码,就建立了一个文件输入流,这个流可以用来把数据从硬盘的文件,读取到JVM(内存)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package stream;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class TestStream {

public static void main(String[] args) {
try {
File f = new File("d:/lol.txt");
// 创建基于文件的输入流
FileInputStream fis = new FileInputStream(f);
// 通过这个输入流,就可以把数据从硬盘,读取到Java的虚拟机中来,也就是读取到内存中

} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}

字节流

InputStream字节输入流
OutputStream字节输出流
用于以字节的形式读取和写入数据

ASCII码

所有的数据存放在计算机中都是以数字的形式存放的。 所以字母就需要转换为数字才能够存放。

比如A就对应的数字65,a对应的数字97. 不同的字母和符号对应不同的数字,就是一张码表。

ASCII是这样的一种码表。 只包含简单的英文字母,符号,数字等等。 不包含中文,德文,俄语等复杂的。

示例中列出了可见的ASCII码以及对应的十进制和十六进制数字,不可见的暂未列出。

字符 十进制数字 十六进制数字
! 33 21
34 22
# 35 23
$ 36 24
% 37 25
& 38 26
39 27
( 40 28
) 41 29
* 42 2A
+ 43 2B
, 44 2C
- 45 2D
. 46 2E
/ 47 2F
0 48 30
1 49 31
2 50 32
3 51 33
4 52 34
5 53 35
6 54 36
7 55 37
8 56 38
9 57 39
: 58 3A
; 59 3B
< 60 3C
= 61 3D
> 62 3E
@ 64 40
A 65 41
B 66 42
C 67 43
D 68 44
E 69 45
F 70 46
G 71 47
H 72 48
I 73 49
J 74 4A
K 75 4B
L 76 4C
M 77 4D
N 78 4E
O 79 4F
P 80 50
Q 81 51
R 82 52
S 83 53
T 84 54
U 85 55
V 86 56
W 87 57
X 88 58
Y 89 59
Z 90 5A
[ 91 5B
\ 92 5C
] 93 5D
^ 94 5E
_ 95 5F
` 96 60
a 97 61
b 98 62
c 99 63
d 100 64
e 101 65
f 102 66
g 103 67
h 104 68
i 105 69
j 106 6A
k 107 6B
l 108 6C
m 109 6D
n 110 6E
o 111 6F
p 112 70
q 113 71
r 114 72
s 115 73
t 116 74
u 117 75
v 118 76
w 119 77
x 120 78
y 121 79
z 122 7A
{ 123 7B
\ 124 7C
} 125 7D
~ 126 7E

以字节流的形式读取文件内容

InputStream是字节输入流,同时也是抽象类,只提供方法声明,不提供方法的具体实现。
FileInputStream 是InputStream子类,以FileInputStream 为例进行文件读取

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
package stream;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class TestStream {

public static void main(String[] args) {
try {
//准备文件lol.txt其中的内容是AB,对应的ASCII分别是65 66
File f =new File("d:/lol.txt");
//创建基于文件的输入流
FileInputStream fis =new FileInputStream(f);
//创建字节数组,其长度就是文件的长度
byte[] all =new byte[(int) f.length()];
//以字节流的形式读取文件所有内容
fis.read(all);
for (byte b : all) {
//打印出来是65 66
System.out.println(b);
}

//每次使用完流,都应该进行关闭
fis.close();

} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}

以字节流的形式向文件写入数据

OutputStream是字节输出流,同时也是抽象类,只提供方法声明,不提供方法的具体实现。
FileOutputStream 是OutputStream子类,以FileOutputStream 为例向文件写出数据

注: 如果文件d:/lol2.txt不存在,写出操作会自动创建该文件。
但是如果是文件 d:/xyz/lol2.txt,而目录xyz又不存在,会抛出异常。

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
package stream;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

public class TestStream {

public static void main(String[] args) {
try {
// 准备文件lol2.txt其中的内容是空的
File f = new File("d:/lol2.txt");
// 准备长度是2的字节数组,用88,89初始化,其对应的字符分别是X,Y
byte data[] = { 88, 89 };

// 创建基于文件的输出流
FileOutputStream fos = new FileOutputStream(f);
// 把数据写入到输出流
fos.write(data);
// 关闭输出流
fos.close();

} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}

关闭流的方式

在try中关闭

在try的作用域里关闭文件输入流,在前面的示例中都是使用这种方式,这样做有一个弊端;
如果文件不存在,或者读取的时候出现问题而抛出异常,那么就不会执行这一行关闭流的代码,存在巨大的资源占用隐患。 不推荐使用

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
package stream;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class TestStream {

public static void main(String[] args) {
try {
File f = new File("d:/lol.txt");
FileInputStream fis = new FileInputStream(f);
byte[] all = new byte[(int) f.length()];
fis.read(all);
for (byte b : all) {
System.out.println(b);
}
// 在try 里关闭流
fis.close();
} catch (IOException e) {
e.printStackTrace();
}

}
}

在finally中关闭

这是标准的关闭流的方式

  1. 首先把流的引用声明在try的外面,如果声明在try里面,其作用域无法抵达finally.
  2. 在finally关闭之前,要先判断该引用是否为空
  3. 关闭的时候,需要再一次进行try catch处理

这是标准的严谨的关闭流的方式,但是看上去很繁琐,所以写不重要的或者测试代码的时候,都会采用上面的有隐患try的方式,因为不麻烦。

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
package stream;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class TestStream {

public static void main(String[] args) {
File f = new File("d:/lol.txt");
FileInputStream fis = null;
try {
fis = new FileInputStream(f);
byte[] all = new byte[(int) f.length()];
fis.read(all);
for (byte b : all) {
System.out.println(b);
}

} catch (IOException e) {
e.printStackTrace();
} finally {
// 在finally 里关闭流
if (null != fis)
try {

fis.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

}
}

使用try()的方式

把流定义在try()里,try,catch或者finally结束的时候,会自动关闭
这种编写代码的方式叫做 try-with-resources, 这是从JDK7开始支持的技术

所有的流,都实现了一个接口叫做 AutoCloseable,任何类实现了这个接口,都可以在try()中进行实例化。 并且在try, catch, finally结束的时候自动关闭,回收相关资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package stream;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class TestStream {

public static void main(String[] args) {
File f = new File("d:/lol.txt");

//把流定义在try()里,try,catch或者finally结束的时候,会自动关闭
try (FileInputStream fis = new FileInputStream(f)) {
byte[] all = new byte[(int) f.length()];
fis.read(all);
for (byte b : all) {
System.out.println(b);
}
} catch (IOException e) {
e.printStackTrace();
}

}
}

字符流 READER/WRITER

Reader字符输入流
Writer字符输出流
专门用于字符的形式读取和写入数据

使用字符流读取文件

FileReader 是Reader子类,以FileReader 为例进行文件读取

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
package stream;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;

public class TestStream {

public static void main(String[] args) {
// 准备文件lol.txt其中的内容是AB
File f = new File("d:/lol.txt");
// 创建基于文件的Reader
try (FileReader fr = new FileReader(f)) {
// 创建字符数组,其长度就是文件的长度
char[] all = new char[(int) f.length()];
// 以字符流的形式读取文件所有内容
fr.read(all);
for (char b : all) {
// 打印出来是A B
System.out.println(b);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}

使用字符流把字符串写入到文件

FileWriter 是Writer的子类,以FileWriter 为例把字符串写入到文件

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
package stream;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

public class TestStream {

public static void main(String[] args) {
// 准备文件lol2.txt
File f = new File("d:/lol2.txt");
// 创建基于文件的Writer
try (FileWriter fr = new FileWriter(f)) {
// 以字符流的形式把数据写入到文件中
String data="abcdefg1234567890";
char[] cs = data.toCharArray();
fr.write(cs);

} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}

中文问题

用FileInputStream 字节流正确读取中文

为了能够正确的读取中文内容

  1. 必须了解文本是以哪种编码方式保存字符的
  2. 使用字节流读取了文本后,再使用对应的编码方式去识别这些数字,得到正确的字符
    如本例,一个文件中的内容是字符,编码方式是GBK,那么读出来的数据一定是D6D0。
    再使用GBK编码方式识别D6D0,就能正确的得到字符

注: 在GBK的棋盘上找到的字后,JVM会自动找到在UNICODE这个棋盘上对应的数字,并且以UNICODE上的数字保存在内存中。

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
package stream;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class TestStream {

public static void main(String[] args) {
File f = new File("E:\\project\\j2se\\src\\test.txt");
try (FileInputStream fis = new FileInputStream(f);) {
byte[] all = new byte[(int) f.length()];
fis.read(all);

//文件中读出来的数据是
System.out.println("文件中读出来的数据是:");
for (byte b : all)
{
int i = b&0x000000ff; //只取16进制的后两位
System.out.println(Integer.toHexString(i));
}
System.out.println("把这个数字,放在GBK的棋盘上去:");
String str = new String(all,"GBK");
System.out.println(str);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}

用FileReader 字符流正确读取中文

FileReader得到的是字符,所以一定是已经把字节根据某种编码识别成了字符了
而FileReader使用的编码方式是Charset.defaultCharset()的返回值,如果是中文的操作系统,就是GBK
FileReader是不能手动设置编码方式的,为了使用其他的编码方式,只能使用InputStreamReader来代替,像这样:

1
new InputStreamReader(new FileInputStream(f),Charset.forName("UTF-8"));

在本例中,用记事本另存为UTF-8格式,然后用UTF-8就能识别对应的中文了。

解释: 为什么中字前面有一个?
如果是使用记事本另存为UTF-8的格式,那么在第一个字节有一个标示符,叫做BOM用来标志这个文件是用UTF-8来编码的。

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
package stream;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;

public class TestStream {

public static void main(String[] args) throws UnsupportedEncodingException, FileNotFoundException {
File f = new File("E:\\project\\j2se\\src\\test.txt");
System.out.println("默认编码方式:"+Charset.defaultCharset());
//FileReader得到的是字符,所以一定是已经把字节根据某种编码识别成了字符了
//而FileReader使用的编码方式是Charset.defaultCharset()的返回值,如果是中文的操作系统,就是GBK
try (FileReader fr = new FileReader(f)) {
char[] cs = new char[(int) f.length()];
fr.read(cs);
System.out.printf("FileReader会使用默认的编码方式%s,识别出来的字符是:%n",Charset.defaultCharset());
System.out.println(new String(cs));
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//FileReader是不能手动设置编码方式的,为了使用其他的编码方式,只能使用InputStreamReader来代替
//并且使用new InputStreamReader(new FileInputStream(f),Charset.forName("UTF-8")); 这样的形式
try (InputStreamReader isr = new InputStreamReader(new FileInputStream(f),Charset.forName("UTF-8"))) {
char[] cs = new char[(int) f.length()];
isr.read(cs);
System.out.printf("InputStreamReader 指定编码方式UTF-8,识别出来的字符是:%n");
System.out.println(new String(cs));
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}

缓存流

以介质是硬盘为例,字节流和字符流的弊端
在每一次读写的时候,都会访问硬盘。 如果读写的频率比较高的时候,其性能表现不佳。

为了解决以上弊端,采用缓存流。
缓存流在读取的时候,会一次性读较多的数据到缓存中,以后每一次的读取,都是在缓存中访问,直到缓存中的数据读取完毕,再到硬盘中读取。

就好比吃饭,不用缓存就是每吃一口都到锅里去铲。用缓存就是先把饭盛到碗里,碗里的吃完了,再到锅里去铲

缓存流在写入数据的时候,会先把数据写入到缓存区,直到缓存区达到一定的量,才把这些数据,一起写入到硬盘中去。按照这种操作模式,就不会像字节流,字符流那样每写一个字节都访问硬盘,从而减少了IO操作。

使用缓存流读取数据

缓存字符输入流 BufferedReader 可以一次读取一行数据

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
package stream;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;

public class TestStream {

public static void main(String[] args) {
// 准备文件lol.txt其中的内容是
// garen kill teemo
// teemo revive after 1 minutes
// teemo try to garen, but killed again
File f = new File("d:/lol.txt");
// 创建文件字符流
// 缓存流必须建立在一个存在的流的基础上
try (
FileReader fr = new FileReader(f);
BufferedReader br = new BufferedReader(fr);
)
{
while (true) {
// 一次读一行
String line = br.readLine();
if (null == line)
break;
System.out.println(line);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}

使用缓存流写出数据

PrintWriter 缓存字符输出流, 可以一次写出一行数据

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
package stream;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;

public class TestStream {

public static void main(String[] args) {
// 向文件lol2.txt中写入三行语句
File f = new File("d:/lol2.txt");

try (
// 创建文件字符流
FileWriter fw = new FileWriter(f);
// 缓存流必须建立在一个存在的流的基础上
PrintWriter pw = new PrintWriter(fw);
) {
pw.println("garen kill teemo");
pw.println("teemo revive after 1 minutes");
pw.println("teemo try to garen, but killed again");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}

flush

有的时候,需要立即把数据写入到硬盘,而不是等缓存满了才写出去。 这时候就需要用到flush

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
package stream;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
public class TestStream {
public static void main(String[] args) {
//向文件lol2.txt中写入三行语句
File f =new File("d:/lol2.txt");
//创建文件字符流
//缓存流必须建立在一个存在的流的基础上
try(FileWriter fr = new FileWriter(f);PrintWriter pw = new PrintWriter(fr);) {
pw.println("garen kill teemo");
//强制把缓存中的数据写入硬盘,无论缓存是否已满
pw.flush();
pw.println("teemo revive after 1 minutes");
pw.flush();
pw.println("teemo try to garen, but killed again");
pw.flush();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

数据流

DataInputStream 数据输入流
DataOutputStream 数据输出流

直接进行字符串的读写

使用数据流的writeUTF()和readUTF() 可以进行数据的格式化顺序读写
如本例,通过DataOutputStream 向文件顺序写出 布尔值,整数和字符串。 然后再通过DataInputStream 顺序读入这些数据。

注: 要用DataInputStream 读取一个文件,这个文件必须是由DataOutputStream 写出的,否则会出现EOFException,因为DataOutputStream 在写出的时候会做一些特殊标记,只有DataInputStream 才能成功的读取。

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
package stream;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class TestStream {

public static void main(String[] args) {
write();
read();
}

private static void read() {
File f =new File("d:/lol.txt");
try (
FileInputStream fis = new FileInputStream(f);
DataInputStream dis =new DataInputStream(fis);
){
boolean b= dis.readBoolean();
int i = dis.readInt();
String str = dis.readUTF();

System.out.println("读取到布尔值:"+b);
System.out.println("读取到整数:"+i);
System.out.println("读取到字符串:"+str);

} catch (IOException e) {
e.printStackTrace();
}

}

private static void write() {
File f =new File("d:/lol.txt");
try (
FileOutputStream fos = new FileOutputStream(f);
DataOutputStream dos =new DataOutputStream(fos);
){
dos.writeBoolean(true);
dos.writeInt(300);
dos.writeUTF("123 this is gareen");
} catch (IOException e) {
e.printStackTrace();
}

}
}

对象流

对象流指的是可以直接把一个对象以流的形式传输给其他的介质,比如硬盘

一个对象以流的形式进行传输,叫做序列化。 该对象所对应的类,必须是实现Serializable接口。

序列化一个对象

创建一个Hero对象,设置其名称为garen。
把该对象序列化到一个文件garen.lol。
然后再通过序列化把该文件转换为一个Hero对象

注:把一个对象序列化有一个前提是:这个对象的类,必须实现了Serializable接口

Hero.java

1
2
3
4
5
6
7
8
9
10
11
package charactor;

import java.io.Serializable;

public class Hero implements Serializable {
//表示这个类当前的版本,如果有了变化,比如新设计了属性,就应该修改这个版本号
private static final long serialVersionUID = 1L;
public String name;
public float hp;

}

TestStream.java

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
package stream;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

import charactor.Hero;

public class TestStream {

public static void main(String[] args) {
//创建一个Hero garen
//要把Hero对象直接保存在文件上,务必让Hero类实现Serializable接口
Hero h = new Hero();
h.name = "garen";
h.hp = 616;

//准备一个文件用于保存该对象
File f =new File("d:/garen.lol");

try(
//创建对象输出流
FileOutputStream fos = new FileOutputStream(f);
ObjectOutputStream oos =new ObjectOutputStream(fos);
//创建对象输入流
FileInputStream fis = new FileInputStream(f);
ObjectInputStream ois =new ObjectInputStream(fis);
) {
oos.writeObject(h);
Hero h2 = (Hero) ois.readObject();
System.out.println(h2.name);
System.out.println(h2.hp);

} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}

System.in

System.out 是常用的在控制台输出数据的
System.in 可以从控制台输入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package stream;

import java.io.IOException;
import java.io.InputStream;

public class TestStream {

public static void main(String[] args) {
// 控制台输入
try (InputStream is = System.in;) {
while (true) {
// 敲入a,然后敲回车可以看到
// 97 13 10
// 97是a的ASCII码
// 13 10分别对应回车换行
int i = is.read();
System.out.println(i);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

Scanner读取字符串

使用System.in.read虽然可以读取数据,但是很不方便
使用Scanner就可以逐行读取了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package stream;

import java.util.Scanner;

public class TestStream {

public static void main(String[] args) {

Scanner s = new Scanner(System.in);

while(true){
String line = s.nextLine();
System.out.println(line);
}

}
}

Scanner从控制台读取整数

1
2
3
4
5
6
7
8
9
10
11
12
13
package stream;

import java.util.Scanner;

public class TestStream {
public static void main(String[] args) {
Scanner s = new Scanner(System.in);
int a = s.nextInt();
System.out.println("第一个整数:"+a);
int b = s.nextInt();
System.out.println("第二个整数:"+b);
}
}

流关系图

这个图把本章节学到的流关系做了个简单整理

  1. 流分为字节流和字符流
  2. 字节流下面常用的又有数据流和对象流
  3. 字符流下面常用的又有缓存流

集合框架

ArrayList

使用数组的局限性

如果要存放多个对象,可以使用数组,但是数组有局限性
比如 声明长度是10的数组
不用的数组就浪费了
超过10的个数,又放不下

TestCollection.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package collection;

import charactor.Hero;

public class TestCollection {
public static void main(String[] args) {
//数组的局限性
Hero heros[] = new Hero[10];
//声明长度是10的数组
//不用的数组就浪费了
//超过10的个数,又放不下
heros[0] = new Hero("盖伦");
//放不下要报错
heros[20] = new Hero("提莫");

}

}

Hero.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package charactor;

public class Hero {
public String name;
public float hp;

public int damage;

public Hero() {

}

// 增加一个初始化name的构造方法
public Hero(String name) {

this.name = name;
}

// 重写toString方法
public String toString() {
return name;
}

}

ArrayList存放对象

为了解决数组的局限性,引入容器类的概念。 最常见的容器类就是
ArrayList
容器的容量”capacity”会随着对象的增加,自动增长
只需要不断往容器里增加英雄即可,不用担心会出现数组的边界问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package collection;

import java.util.ArrayList;

import charactor.Hero;

public class TestCollection {
@SuppressWarnings("rawtypes")
public static void main(String[] args) {
//容器类ArrayList,用于存放对象
ArrayList heros = new ArrayList();
heros.add( new Hero("盖伦"));
System.out.println(heros.size());

//容器的容量"capacity"会随着对象的增加,自动增长
//只需要不断往容器里增加英雄即可,不用担心会出现数组的边界问题。
heros.add( new Hero("提莫"));
System.out.println(heros.size());

}

}

增加

add 有两种用法
第一种是直接add对象,把对象加在最后面

heros.add(new Hero("hero " + i));

第二种是在指定位置加对象

heros.add(3, specialHero);

TestCollection.java

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
package collection;

import java.util.ArrayList;

import charactor.Hero;

public class TestCollection {
public static void main(String[] args) {
ArrayList heros = new ArrayList();

// 把5个对象加入到ArrayList中
for (int i = 0; i < 5; i++) {
heros.add(new Hero("hero " + i));
}
System.out.println(heros);

// 在指定位置增加对象
Hero specialHero = new Hero("special hero");
heros.add(3, specialHero);

System.out.println(heros.toString());

}

}

Hero.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package charactor;

public class Hero {
public String name;
public float hp;

public int damage;

public Hero() {

}

// 增加一个初始化name的构造方法
public Hero(String name) {

this.name = name;
}

// 重写toString方法
public String toString() {
return name;
}

}

判断是否存在

通过方法contains 判断一个对象是否在容器中。
判断标准: 是否是同一个对象,而不是name是否相同。

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
package collection;

import java.util.ArrayList;

import charactor.Hero;

public class TestCollection {
public static void main(String[] args) {
ArrayList heros = new ArrayList();

// 初始化5个对象
for (int i = 0; i < 5; i++) {
heros.add(new Hero("hero " + i));
}
Hero specialHero = new Hero("special hero");
heros.add(specialHero);

System.out.println(heros);
// 判断一个对象是否在容器中
// 判断标准: 是否是同一个对象,而不是name是否相同
System.out.print("虽然一个新的对象名字也叫 hero 1,但是contains的返回是:");
System.out.println(heros.contains(new Hero("hero 1")));
System.out.print("而对specialHero的判断,contains的返回是:");
System.out.println(heros.contains(specialHero));
}

}

获取指定位置的对象

通过get获取指定位置的对象,如果输入的下标越界,一样会报错。

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
package collection;

import java.util.ArrayList;

import charactor.Hero;

public class TestCollection {
public static void main(String[] args) {
ArrayList heros = new ArrayList();

// 初始化5个对象
for (int i = 0; i < 5; i++) {
heros.add(new Hero("hero " + i));
}
Hero specialHero = new Hero("special hero");
heros.add(specialHero);

//获取指定位置的对象
System.out.println(heros.get(5));
//如果超出了范围,依然会报错
System.out.println(heros.get(6));

}

}

获取对象所处的位置

indexOf用于判断一个对象在ArrayList中所处的位置
与contains一样,判断标准是对象是否相同,而非对象的name值是否相等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package collection;

import java.util.ArrayList;

import charactor.Hero;

public class TestCollection {
public static void main(String[] args) {
ArrayList heros = new ArrayList();

// 初始化5个对象
for (int i = 0; i < 5; i++) {
heros.add(new Hero("hero " + i));
}
Hero specialHero = new Hero("special hero");
heros.add(specialHero);

System.out.println(heros);
System.out.println("specialHero所处的位置:"+heros.indexOf(specialHero));
System.out.println("新的英雄,但是名字是\"hero 1\"所处的位置:"+heros.indexOf(new Hero("hero 1")));

}
}

删除

remove用于把对象从ArrayList中删除
remove可以根据下标删除ArrayList的元素

heros.remove(2);

也可以根据对象删除

heros.remove(specialHero);

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
package collection;

import java.util.ArrayList;

import charactor.Hero;

public class TestCollection {
public static void main(String[] args) {
ArrayList heros = new ArrayList();

// 初始化5个对象
for (int i = 0; i < 5; i++) {
heros.add(new Hero("hero " + i));
}
Hero specialHero = new Hero("special hero");
heros.add(specialHero);

System.out.println(heros);
heros.remove(2);
System.out.println("删除下标是2的对象");
System.out.println(heros);
System.out.println("删除special hero");
heros.remove(specialHero);
System.out.println(heros);

}
}

替换

set用于替换指定位置的元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package collection;

import java.util.ArrayList;

import charactor.Hero;

public class TestCollection {
public static void main(String[] args) {
ArrayList heros = new ArrayList();

// 初始化5个对象
for (int i = 0; i < 5; i++) {
heros.add(new Hero("hero " + i));
}
Hero specialHero = new Hero("special hero");
heros.add(specialHero);

System.out.println(heros);
System.out.println("把下标是5的元素,替换为\"hero 5\"");
heros.set(5, new Hero("hero 5"));
System.out.println(heros);
}
}

获取大小

size 用于获取ArrayList的大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package collection;

import java.util.ArrayList;

import charactor.Hero;

public class TestCollection {
public static void main(String[] args) {
ArrayList heros = new ArrayList();

// 初始化5个对象
for (int i = 0; i < 5; i++) {
heros.add(new Hero("hero " + i));
}
Hero specialHero = new Hero("special hero");
heros.add(specialHero);
System.out.println(heros);
System.out.println("获取ArrayList的大小:");
System.out.println(heros.size());
}
}

转换为数组

toArray可以把一个ArrayList对象转换为数组。
需要注意的是,如果要转换为一个Hero数组,那么需要传递一个Hero数组类型的对象给toArray(),这样toArray方法才知道,你希望转换为哪种类型的数组,否则只能转换为Object数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package collection;

import java.util.ArrayList;

import charactor.Hero;

public class TestCollection {
public static void main(String[] args) {
ArrayList heros = new ArrayList();

// 初始化5个对象
for (int i = 0; i < 5; i++) {
heros.add(new Hero("hero " + i));
}
Hero specialHero = new Hero("special hero");
heros.add(specialHero);
System.out.println(heros);
Hero hs[] = (Hero[])heros.toArray(new Hero[]{});
System.out.println("数组:" +hs);

}
}

把另一个容器所有对象都加进来

addAll 把另一个容器所有对象都加进。

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
package collection;

import java.util.ArrayList;

import charactor.Hero;

public class TestCollection {
public static void main(String[] args) {
ArrayList heros = new ArrayList();

// 初始化5个对象
for (int i = 0; i < 5; i++) {
heros.add(new Hero("hero " + i));
}

System.out.println("ArrayList heros:\t" + heros);

//把另一个容器里所有的元素,都加入到该容器里来
ArrayList anotherHeros = new ArrayList();
anotherHeros.add(new Hero("hero a"));
anotherHeros.add(new Hero("hero b"));
anotherHeros.add(new Hero("hero c"));
System.out.println("anotherHeros heros:\t" + anotherHeros);
heros.addAll(anotherHeros);
System.out.println("把另一个ArrayList的元素都加入到当前ArrayList:");
System.out.println("ArrayList heros:\t" + heros);

}
}

清空

clear 清空一个ArrayList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package collection;

import java.util.ArrayList;

import charactor.Hero;

public class TestCollection {
public static void main(String[] args) {
ArrayList heros = new ArrayList();

// 初始化5个对象
for (int i = 0; i < 5; i++) {
heros.add(new Hero("hero " + i));
}

System.out.println("ArrayList heros:\t" + heros);
System.out.println("使用clear清空");
heros.clear();
System.out.println("ArrayList heros:\t" + heros);

}
}

List接口

ArrayList和List

ArrayList实现了接口List
常见的写法会把引用声明为接口List类型
注意:是java.util.List,而不是java.awt.List

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package collection;

import java.util.ArrayList;
import java.util.List;

import charactor.Hero;

public class TestCollection {

public static void main(String[] args) {
//ArrayList实现了接口List

//常见的写法会把引用声明为接口List类型
//注意:是java.util.List,而不是java.awt.List
//接口引用指向子类对象(多态)

List heros = new ArrayList();
heros.add( new Hero("盖伦"));
System.out.println(heros.size());

}

}

在ArrayList上使用泛型

泛型 Generic

不指定泛型的容器,可以存放任何类型的元素
指定了泛型的容器,只能存放指定类型的元素以及其子类

Item.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package property;

public class Item {
String name;
int price;

public Item(){

}

//提供一个初始化name的构造方法
public Item(String name){
this.name = name;
}

public void effect(){
System.out.println("物品使用后,可以有效果");
}

}

TestCollection.java

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
package collection;

import java.util.ArrayList;
import java.util.List;

import property.Item;
import charactor.APHero;
import charactor.Hero;

public class TestCollection {

public static void main(String[] args) {

//对于不使用泛型的容器,可以往里面放英雄,也可以往里面放物品
List heros = new ArrayList();

heros.add(new Hero("盖伦"));

//本来用于存放英雄的容器,现在也可以存放物品了
heros.add(new Item("冰杖"));

//对象转型会出现问题
Hero h1= (Hero) heros.get(0);
//尤其是在容器里放的对象太多的时候,就记不清楚哪个位置放的是哪种类型的对象了
Hero h2= (Hero) heros.get(1);

//引入泛型Generic
//声明容器的时候,就指定了这种容器,只能放Hero,放其他的就会出错
List<Hero> genericheros = new ArrayList<Hero>();
genericheros.add(new Hero("盖伦"));
//如果不是Hero类型,根本就放不进去
//genericheros.add(new Item("冰杖"));

//除此之外,还能存放Hero的子类
genericheros.add(new APHero());

//并且在取出数据的时候,不需要再进行转型了,因为里面肯定是放的Hero或者其子类
Hero h = genericheros.get(0);

}

}

泛型的简写

为了不使编译器出现警告,需要前后都使用泛型,像这样:

List<Hero> genericheros = new ArrayList<Hero>();

不过JDK7提供了一个可以略微减少代码量的泛型简写方式

List<Hero> genericheros2 = new ArrayList<>();

后面的泛型可以用<>来代替,聊胜于无吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package collection;

import java.util.ArrayList;
import java.util.List;

import charactor.Hero;

public class TestCollection {

public static void main(String[] args) {
List<Hero> genericheros = new ArrayList<Hero>();
List<Hero> genericheros2 = new ArrayList<>();

}

}

遍历ArrayList

用for循环遍历

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
package collection;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import charactor.Hero;

public class TestCollection {

public static void main(String[] args) {
List<Hero> heros = new ArrayList<Hero>();

// 放5个Hero进入容器
for (int i = 0; i < 5; i++) {
heros.add(new Hero("hero name " + i));
}

// 第一种遍历 for循环
System.out.println("--------for 循环-------");
for (int i = 0; i < heros.size(); i++) {
Hero h = heros.get(i);
System.out.println(h);
}

}

}

迭代器遍历

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
package collection;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import charactor.Hero;

public class TestCollection {

public static void main(String[] args) {
List<Hero> heros = new ArrayList<Hero>();

//放5个Hero进入容器
for (int i = 0; i < 5; i++) {
heros.add(new Hero("hero name " +i));
}

//第二种遍历,使用迭代器
System.out.println("--------使用while的iterator-------");
Iterator<Hero> it= heros.iterator();
//从最开始的位置判断"下一个"位置是否有数据
//如果有就通过next取出来,并且把指针向下移动
//直到"下一个"位置没有数据
while(it.hasNext()){
Hero h = it.next();
System.out.println(h);
}
//迭代器的for写法
System.out.println("--------使用for的iterator-------");
for (Iterator<Hero> iterator = heros.iterator(); iterator.hasNext();) {
Hero hero = (Hero) iterator.next();
System.out.println(hero);
}

}

}

用增强型for循环

使用增强型for循环可以非常方便的遍历ArrayList中的元素,这是很多开发人员的首选。

不过增强型for循环也有不足:
无法用来进行ArrayList的初始化
无法得知当前是第几个元素了,当需要只打印单数元素的时候,就做不到了。 必须再自定下标变量。

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
package collection;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import charactor.Hero;

public class TestCollection {

public static void main(String[] args) {
List<Hero> heros = new ArrayList<Hero>();

// 放5个Hero进入容器
for (int i = 0; i < 5; i++) {
heros.add(new Hero("hero name " + i));
}

// 第三种,增强型for循环
System.out.println("--------增强型for循环-------");
for (Hero h : heros) {
System.out.println(h);
}

}

}

LinkedList

序列分先进先出FIFO,先进后出FILO
FIFO在Java中又叫Queue 队列
FILO在Java中又叫Stack 栈

LinkedList 与 List接口

与ArrayList一样,LinkedList也实现了List接口,诸如add, remove, contains等等方法。

双向链表 - Deque

除了实现了List接口外,LinkedList还实现了双向链表结构Deque,可以很方便的在头尾插入删除数据

什么是链表结构: 与数组结构相比较,数组结构,就好像是电影院,每个位置都有标示,每个位置之间的间隔都是一样的。 而链表就相当于佛珠,每个珠子,只连接前一个和后一个,不用关心除此之外的其他佛珠在哪里。

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
package collection;

import java.util.LinkedList;

import charactor.Hero;

public class TestCollection {

public static void main(String[] args) {

//LinkedList是一个双向链表结构的list
LinkedList<Hero> ll =new LinkedList<Hero>();

//所以可以很方便的在头部和尾部插入数据
//在最后插入新的英雄
ll.addLast(new Hero("hero1"));
ll.addLast(new Hero("hero2"));
ll.addLast(new Hero("hero3"));
System.out.println(ll);

//在最前面插入新的英雄
ll.addFirst(new Hero("heroX"));
System.out.println(ll);

//查看最前面的英雄
System.out.println(ll.getFirst());
//查看最后面的英雄
System.out.println(ll.getLast());

//查看不会导致英雄被删除
System.out.println(ll);
//取出最前面的英雄
System.out.println(ll.removeFirst());

//取出最后面的英雄
System.out.println(ll.removeLast());

//取出会导致英雄被删除
System.out.println(ll);

}

}

队列 - Queue

LinkedList 除了实现了List和Deque外,还实现了Queue接口(队列)。
Queue是先进先出队列 FIFO,常用方法:

  • offer 在最后添加元素
  • poll 取出第一个元素
  • peek 查看第一个元素

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
package collection;

import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

import charactor.Hero;

public class TestCollection {

public static void main(String[] args) {
//和ArrayList一样,LinkedList也实现了List接口
List ll =new LinkedList<Hero>();

//所不同的是LinkedList还实现了Deque,进而又实现了Queue这个接口
//Queue代表FIFO 先进先出的队列
Queue<Hero> q= new LinkedList<Hero>();

//加在队列的最后面
System.out.print("初始化队列:\t");
q.offer(new Hero("Hero1"));
q.offer(new Hero("Hero2"));
q.offer(new Hero("Hero3"));
q.offer(new Hero("Hero4"));

System.out.println(q);
System.out.print("把第一个元素取poll()出来:\t");
//取出第一个Hero,FIFO 先进先出
Hero h = q.poll();
System.out.println(h);
System.out.print("取出第一个元素之后的队列:\t");
System.out.println(q);

//把第一个拿出来看一看,但是不取出来
h=q.peek();
System.out.print("查看peek()第一个元素:\t");
System.out.println(h);
System.out.print("查看并不会导致第一个元素被取出来:\t");
System.out.println(q);

}

}

二叉树

二叉树概念

二叉树由各种节点组成
二叉树特点:
每个节点都可以有左子节点,右子节点
每一个节点都有一个

1
2
3
4
5
6
7
8
9
10
package collection;

public class Node {
// 左子节点
public Node leftNode;
// 右子节点
public Node rightNode;
// 值
public Object value;
}

二叉树排序 - 插入数据

假设通过二叉树对如下10个随机数进行排序
67,7,30,73,10,0,78,81,10,74
排序的第一个步骤是把数据插入到该二叉树中
插入基本逻辑是,小、相同的放左边,大的放右边

  1. 67 放在根节点
  2. 7 比 67小,放在67的左节点
  3. 30 比67 小,找到67的左节点7,30比7大,就放在7的右节点
  4. 73 比67大, 放在67的右节点
  5. 10 比 67小,找到67的左节点7,10比7大,找到7的右节点30,10比30小,放在30的左节点。

  6. 10比67小,找到67的左节点7,10比7大,找到7的右节点30,10比30小,找到30的左节点10,10和10一样大,放在左边

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
package collection;

public class Node {
// 左子节点
public Node leftNode;
// 右子节点
public Node rightNode;

// 值
public Object value;

// 插入 数据
public void add(Object v) {
// 如果当前节点没有值,就把数据放在当前节点上
if (null == value)
value = v;

// 如果当前节点有值,就进行判断,新增的值与当前值的大小关系
else {
// 新增的值,比当前值小或者相同

if ((Integer) v -((Integer)value) <= 0) {
if (null == leftNode)
leftNode = new Node();
leftNode.add(v);
}
// 新增的值,比当前值大
else {
if (null == rightNode)
rightNode = new Node();
rightNode.add(v);
}

}

}

public static void main(String[] args) {

int randoms[] = new int[] { 67, 7, 30, 73, 10, 0, 78, 81, 10, 74 };

Node roots = new Node();
for (int number : randoms) {
roots.add(number);
}

}
}

二叉树排序 - 遍历

通过上一个步骤的插入行为,实际上,数据就已经排好序了。 接下来要做的是看,把这些已经排好序的数据,遍历成我们常用的List或者数组的形式

二叉树的遍历分左序,中序,右序
左序即: 中间的数遍历后放在左边
中序即: 中间的数遍历后放在中间
右序即: 中间的数遍历后放在右边
如图所见,我们希望遍历后的结果是从小到大的,所以应该采用中序遍历

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
67
68
69
70
71
72
package collection;

import java.util.ArrayList;
import java.util.List;

public class Node {
// 左子节点
public Node leftNode;
// 右子节点
public Node rightNode;

// 值
public Object value;

// 插入 数据
public void add(Object v) {
// 如果当前节点没有值,就把数据放在当前节点上
if (null == value)
value = v;

// 如果当前节点有值,就进行判断,新增的值与当前值的大小关系
else {
// 新增的值,比当前值小或者相同

if ((Integer) v -((Integer)value) <= 0) {
if (null == leftNode)
leftNode = new Node();
leftNode.add(v);
}
// 新增的值,比当前值大
else {
if (null == rightNode)
rightNode = new Node();
rightNode.add(v);
}

}

}

// 中序遍历所有的节点
public List<Object> values() {
List<Object> values = new ArrayList<>();

// 左节点的遍历结果
if (null != leftNode)
values.addAll(leftNode.values());

// 当前节点
values.add(value);

// 右节点的遍历结果
if (null != rightNode)

values.addAll(rightNode.values());

return values;
}

public static void main(String[] args) {

int randoms[] = new int[] { 67, 7, 30, 73, 10, 0, 78, 81, 10, 74 };

Node roots = new Node();
for (int number : randoms) {
roots.add(number);
}

System.out.println(roots.values());

}
}

HashMap

HashMap的键值对

HashMap储存数据的方式是 —— 键值对

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package collection;

import java.util.HashMap;

public class TestCollection {
public static void main(String[] args) {
HashMap<String,String> dictionary = new HashMap<>();
dictionary.put("adc", "物理英雄");
dictionary.put("apc", "魔法英雄");
dictionary.put("t", "坦克");

System.out.println(dictionary.get("t"));
}
}

键不能重复,值可以重复

对于HashMap而言,key是唯一的,不可以重复的。
所以,以相同的key 把不同的value插入到 Map中会导致旧元素被覆盖,只留下最后插入的元素。
不过,同一个对象可以作为值插入到map中,只要对应的key不一样。

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
package collection;

import java.util.HashMap;

import charactor.Hero;

public class TestCollection {
public static void main(String[] args) {
HashMap<String,Hero> heroMap = new HashMap<String,Hero>();

heroMap.put("gareen", new Hero("gareen1"));
System.out.println(heroMap);

//key为gareen已经有value了,再以gareen作为key放入数据,会导致原英雄,被覆盖
//不会增加新的元素到Map中
heroMap.put("gareen", new Hero("gareen2"));
System.out.println(heroMap);

//清空map
heroMap.clear();
Hero gareen = new Hero("gareen");

//同一个对象可以作为值插入到map中,只要对应的key不一样
heroMap.put("hero1", gareen);
heroMap.put("hero2", gareen);

System.out.println(heroMap);

}
}

HashSet

元素不能重复

Set中的元素,不能重复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package collection;

import java.util.HashSet;

public class TestCollection {
public static void main(String[] args) {

HashSet<String> names = new HashSet<String>();

names.add("gareen");

System.out.println(names);

//第二次插入同样的数据,是插不进去的,容器中只会保留一个
names.add("gareen");
System.out.println(names);
}
}

没有顺序

Set中的元素,没有顺序。
严格的说,是没有按照元素的插入顺序排列

HashSet的具体顺序,既不是按照插入顺序,也不是按照hashcode的顺序。关于hashcode有专门的章节讲解: hashcode 原理。

以下是HashSet源代码中的部分注释

1
2
3
4
/**
* It makes no guarantees as to the iteration order of the set;
* in particular, it does not guarantee that the order will remain constant over time.
*/

不保证Set的迭代顺序; 确切的说,在不同条件下,元素的顺序都有可能不一样。

换句话说,同样是插入0-9到HashSet中, 在JVM的不同版本中,看到的顺序都是不一样的。 所以在开发的时候,不能依赖于某种臆测的顺序,这个顺序本身是不稳定的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package collection;

import java.util.HashSet;

public class TestCollection {
public static void main(String[] args) {
HashSet<Integer> numbers = new HashSet<Integer>();

numbers.add(9);
numbers.add(5);
numbers.add(1);

// Set中的元素排列,不是按照插入顺序
System.out.println(numbers);

}
}

遍历

Set不提供get()来获取指定位置的元素,所以遍历需要用到迭代器,或者增强型for循环。

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
package collection;

import java.util.HashSet;
import java.util.Iterator;

public class TestCollection {
public static void main(String[] args) {
HashSet<Integer> numbers = new HashSet<Integer>();

for (int i = 0; i < 20; i++) {
numbers.add(i);
}

//Set不提供get方法来获取指定位置的元素
//numbers.get(0)

//遍历Set可以采用迭代器iterator
for (Iterator<Integer> iterator = numbers.iterator(); iterator.hasNext();) {
Integer i = (Integer) iterator.next();
System.out.println(i);
}

//或者采用增强型for循环
for (Integer i : numbers) {
System.out.println(i);
}

}
}

HashSet和HashMap的关系

通过观察HashSet的源代码(如何查看源代码)
可以发现HashSet自身并没有独立的实现,而是在里面封装了一个Map.
HashSet是作为Map的key而存在的
而value是一个命名为PRESENT的static的Object对象,因为是一个类属性,所以只会有一个。

private static final Object PRESENT = new Object();

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
package collection;

import java.util.AbstractSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;

public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
//HashSet里封装了一个HashMap
private HashMap<E,Object> map;

private static final Object PRESENT = new Object();

//HashSet的构造方法初始化这个HashMap
public HashSet() {
map = new HashMap<E,Object>();
}

//向HashSet中增加元素,其实就是把该元素作为key,增加到Map中
//value是PRESENT,静态,final的对象,所有的HashSet都使用这么同一个对象
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}

//HashSet的size就是map的size
public int size() {
return map.size();
}

//清空Set就是清空Map
public void clear() {
map.clear();
}

//迭代Set,就是把Map的键拿出来迭代
public Iterator<E> iterator() {
return map.keySet().iterator();
}

}

Collection & Collection

Collection是 Set、List、Queue和 Deque 的接口
Queue: 先进先出队列
Deque: 双向链表

注:Collection和Map之间没有关系,Collection是放一个一个对象的,Map 是放键值对的。
注:Deque 继承 Queue,间接的继承了 Collection。

Collections

Collections是一个类,容器的工具类,就如同Arrays是数组的工具类。

反转

reverse 使List中的数据发生翻转

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
package collection;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class TestCollection {
public static void main(String[] args) {
//初始化集合numbers
List<Integer> numbers = new ArrayList<>();

for (int i = 0; i < 10; i++) {
numbers.add(i);
}

System.out.println("集合中的数据:");
System.out.println(numbers);

Collections.reverse(numbers);

System.out.println("翻转后集合中的数据:");
System.out.println(numbers);

}
}

混淆

shuffle 混淆List中数据的顺序

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
package collection;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class TestCollection {
public static void main(String[] args) {
//初始化集合numbers
List<Integer> numbers = new ArrayList<>();

for (int i = 0; i < 10; i++) {
numbers.add(i);
}

System.out.println("集合中的数据:");
System.out.println(numbers);

Collections.shuffle(numbers);

System.out.println("混淆后集合中的数据:");
System.out.println(numbers);

}
}

排序

sort 对List中的数据进行排序

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
package collection;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class TestCollection {
public static void main(String[] args) {
//初始化集合numbers
List<Integer> numbers = new ArrayList<>();

for (int i = 0; i < 10; i++) {
numbers.add(i);
}

System.out.println("集合中的数据:");
System.out.println(numbers);

Collections.shuffle(numbers);
System.out.println("混淆后集合中的数据:");
System.out.println(numbers);

Collections.sort(numbers);
System.out.println("排序后集合中的数据:");
System.out.println(numbers);

}
}

交换

swap 交换两个数据的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package collection;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class TestCollection {
public static void main(String[] args) {
//初始化集合numbers
List<Integer> numbers = new ArrayList<>();

for (int i = 0; i < 10; i++) {
numbers.add(i);
}

System.out.println("集合中的数据:");
System.out.println(numbers);

Collections.swap(numbers,0,5);
System.out.println("交换0和5下标的数据后,集合中的数据:");
System.out.println(numbers);

}
}

滚动

rotate 把List中的数据,向右滚动指定单位的长度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package collection;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class TestCollection {
public static void main(String[] args) {
//初始化集合numbers
List<Integer> numbers = new ArrayList<>();

for (int i = 0; i < 10; i++) {
numbers.add(i);
}

System.out.println("集合中的数据:");
System.out.println(numbers);

Collections.rotate(numbers,2);
System.out.println("把集合向右滚动2个单位,标的数据后,集合中的数据:");
System.out.println(numbers);

}
}

线程安全化

synchronizedList 把非线程安全的List转换为线程安全的List。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package collection;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class TestCollection {
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>();

System.out.println("把非线程安全的List转换为线程安全的List");
List<Integer> synchronizedNumbers = (List<Integer>) Collections.synchronizedList(numbers);

}
}

ArrayList vs HashSet

是否有顺序

ArrayList: 有顺序
HashSet: 无顺序

HashSet的具体顺序,既不是按照插入顺序,也不是按照hashcode的顺序。关于hashcode有专门的章节讲解: hashcode 原理。

以下是HasetSet源代码中的部分注释

1
2
3
4
/**
* It makes no guarantees as to the iteration order of the set;
* in particular, it does not guarantee that the order will remain constant over time.
*/

不保证Set的迭代顺序; 确切的说,在不同条件下,元素的顺序都有可能不一样

换句话说,同样是插入0-9到HashSet中, 在JVM的不同版本中,看到的顺序都是不一样的。 所以在开发的时候,不能依赖于某种臆测的顺序,这个顺序本身是不稳定的。

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
package collection;

import java.util.ArrayList;
import java.util.HashSet;

public class TestCollection {
public static void main(String[] args) {

ArrayList<Integer> numberList =new ArrayList<Integer>();
//List中的数据按照插入顺序存放
System.out.println("----------List----------");
System.out.println("向List 中插入 9 5 1");
numberList.add(9);
numberList.add(5);
numberList.add(1);
System.out.println("List 按照顺序存放数据:");
System.out.println(numberList);
System.out.println("----------Set----------");
HashSet<Integer> numberSet =new HashSet<Integer>();
System.out.println("向Set 中插入9 5 1");
//Set中的数据不是按照插入顺序存放
numberSet.add(9);
numberSet.add(5);
numberSet.add(1);
System.out.println("Set 不是按照顺序存放数据:");
System.out.println(numberSet);

}
}

能否重复

List中的数据可以重复
Set中的数据不能够重复。
重复判断标准是:
首先看hashcode是否相同

  • 如果hashcode不同,则认为是不同数据
  • 如果hashcode相同,再比较equals,如果equals相同,则是相同数据,否则是不同数据

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
package collection;

import java.util.ArrayList;
import java.util.HashSet;

public class TestCollection {
public static void main(String[] args) {

ArrayList<Integer> numberList =new ArrayList<Integer>();
//List中的数据可以重复
System.out.println("----------List----------");
System.out.println("向List 中插入 9 9");
numberList.add(9);
numberList.add(9);
System.out.println("List 中出现两个9:");
System.out.println(numberList);
System.out.println("----------Set----------");
HashSet<Integer> numberSet =new HashSet<Integer>();
System.out.println("向Set 中插入9 9");
//Set中的数据不能重复
numberSet.add(9);
numberSet.add(9);
System.out.println("Set 中只会保留一个9:");
System.out.println(numberSet);

}
}

ArrayList vs LinkedList

ArrayList和LinkedList的区别

ArrayList 插入、删除数据慢
LinkedList 插入、删除数据快
ArrayList是顺序结构,所以定位很快,指哪找哪。 就像电影院位置一样,有了电影票,一下就找到位置了。
LinkedList 是链表结构,就像手里的一串佛珠,要找出第99个佛珠,必须得一个一个的数过去,所以定位慢

插入数据

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
package collection;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class TestCollection {
public static void main(String[] args) {
List<Integer> l;
l = new ArrayList<>();
insertFirst(l, "ArrayList");

l = new LinkedList<>();
insertFirst(l, "LinkedList");

}

private static void insertFirst(List<Integer> l, String type) {
int total = 1000 * 100;
final int number = 5;
long start = System.currentTimeMillis();
for (int i = 0; i < total; i++) {
l.add(0, number);
}
long end = System.currentTimeMillis();
System.out.printf("在%s 最前面插入%d条数据,总共耗时 %d 毫秒 %n", type, total, end - start);
}

}

定位数据

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
package collection;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class TestCollection {
public static void main(String[] args) {
List<Integer> l;
l = new ArrayList<>();
modify(l, "ArrayList");

l = new LinkedList<>();
modify(l, "LinkedList");

}

private static void modify(List<Integer> l, String type) {
int total = 100 * 1000;
int index = total/2;
final int number = 5;
//初始化
for (int i = 0; i < total; i++) {
l.add(number);
}

long start = System.currentTimeMillis();

for (int i = 0; i < total; i++) {
int n = l.get(index);
n++;
l.set(index, n);
}
long end = System.currentTimeMillis();
System.out.printf("%s总长度是%d,定位到第%d个数据,取出来,加1,再放回去%n 重复%d遍,总共耗时 %d 毫秒 %n", type,total, index,total, end - start);
System.out.println();
}

}

HashMap vs HashTable

HashMap和Hashtable的区别

HashMap和Hashtable都实现了Map接口,都是键值对保存数据的方式
区别1:

  • HashMap可以存放 null
  • Hashtable不能存放null

区别2:

  • HashMap不是线程安全的类
  • Hashtable是线程安全的类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package collection;

import java.util.HashMap;
import java.util.Hashtable;

public class TestCollection {
public static void main(String[] args) {

//HashMap和Hashtable都实现了Map接口,都是键值对保存数据的方式

HashMap<String,String> hashMap = new HashMap<String,String>();

//HashMap可以用null作key,作value
hashMap.put(null, "123");
hashMap.put("123", null);

Hashtable<String,String> hashtable = new Hashtable<String,String>();
//Hashtable不能用null作key,不能用null作value
hashtable.put(null, "123");
hashtable.put("123", null);

}
}

Hashcode 原理

List查找的低效率

假设在List中存放着无重复名称,没有顺序的2000000个Hero,要把名字叫做“hero 1000000”的对象找出来。

List的做法是对每一个进行挨个遍历,直到找到名字叫做“hero 1000000”的英雄。
最差的情况下,需要遍历和比较2000000次,才能找到对应的英雄。

测试逻辑:

  1. 初始化2000000个对象到ArrayList中
  2. 打乱容器中的数据顺序
  3. 进行10次查询,统计每一次消耗的时间

不同计算机的配置情况下,所花的时间是有区别的。 在本机上,花掉的时间大概是600毫秒左右。

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
package collection;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import charactor.Hero;

public class TestCollection {
public static void main(String[] args) {
List<Hero> heros = new ArrayList<Hero>();

for (int j = 0; j < 2000000; j++) {
Hero h = new Hero("Hero " + j);
heros.add(h);
}

// 进行10次查找,观察大体的平均值
for (int i = 0; i < 10; i++) {
// 打乱heros中元素的顺序
Collections.shuffle(heros);

long start = System.currentTimeMillis();

String target = "Hero 1000000";

for (Hero hero : heros) {
if (hero.name.equals(target)) {
System.out.println("找到了 hero!" );
break;
}
}
long end = System.currentTimeMillis();
long elapsed = end - start;
System.out.println("一共花了:" + elapsed + " 毫秒");
}

}
}

HashMap的性能表现

使用HashMap 做同样的查找

  1. 初始化2000000个对象到HashMap中。
  2. 进行10次查询
  3. 统计每一次的查询消耗的时间

可以观察到,几乎不花时间,花费的时间在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
28
29
30
package collection;

import java.util.HashMap;

import charactor.Hero;

public class TestCollection {
public static void main(String[] args) {

HashMap<String,Hero> heroMap = new HashMap<String,Hero>();
for (int j = 0; j < 2000000; j++) {
Hero h = new Hero("Hero " + j);
heroMap.put(h.name, h);
}
System.out.println("数据准备完成");

for (int i = 0; i < 10; i++) {
long start = System.currentTimeMillis();

//查找名字是Hero 1000000的对象
Hero target = heroMap.get("Hero 1000000");
System.out.println("找到了 hero!" + target.name);

long end = System.currentTimeMillis();
long elapsed = end - start;
System.out.println("一共花了:" + elapsed + " 毫秒");
}

}
}

HashMap原理与字典

在展开HashMap原理的讲解之前,首先回忆一下大家初中和高中使用的汉英字典。

比如要找一个单词对应的中文意思,假设单词是Lengendary,首先在目录找到Lengendary在第 555页。

然后,翻到第555页,这页不只一个单词,但是量已经很少了,逐一比较,很快就定位目标单词Lengendary。

555相当于就是Lengendary对应的hashcode

分析HashMap性能卓越的原因

-----hashcode概念-----
所有的对象,都有一个对应的hashcode(散列值)
比如字符串“gareen”对应的是1001 (实际上不是,这里是方便理解,假设的值)
比如字符串“temoo”对应的是1004
比如字符串“db”对应的是1008
比如字符串“annie”对应的也是1008

-----保存数据-----
准备一个数组,其长度是2000,并且设定特殊的hashcode算法,使得所有字符串对应的hashcode,都会落在0-1999之间
要存放名字是”gareen”的英雄,就把该英雄和名称组成一个键值对,存放在数组的1001这个位置上
要存放名字是”temoo”的英雄,就把该英雄存放在数组的1004这个位置上
要存放名字是”db”的英雄,就把该英雄存放在数组的1008这个位置上
要存放名字是”annie”的英雄,然而 “annie”的hashcode 1008对应的位置已经有db英雄了,那么就在这里创建一个链表,接在db英雄后面存放annie

-----查找数据-----
比如要查找gareen,首先计算”gareen”的hashcode是1001,根据1001这个下标,到数组中进行定位,(根据数组下标进行定位,是非常快速的) 发现1001这个位置就只有一个英雄,那么该英雄就是gareen.
比如要查找annie,首先计算”annie”的hashcode是1008,根据1008这个下标,到数组中进行定位,发现1008这个位置有两个英雄,那么就对两个英雄的名字进行逐一比较(equals),因为此时需要比较的量就已经少很多了,很快也就可以找出目标英雄
这就是使用hashmap进行查询,非常快原理。

这是一种用空间换时间的思维方式。

HashSet判断是否重复

HashSet的数据是不能重复的,相同数据不能保存在一起,到底如何判断是否是重复的呢?
根据HashSet和HashMap的关系,我们了解到因为HashSet没有自身的实现,而是里面封装了一个HashMap,所以本质上就是判断HashMap的key是否重复。

再通过上一步的学习,key是否重复,是由两个步骤判断的:
hashcode是否一样
如果hashcode不一样,就是在不同的坑里,一定是不重复的
如果hashcode一样,就是在同一个坑里,还需要进行equals比较
如果equals一样,则是重复数据
如果equals不一样,则是不同数据。

比较器

Comparator

假设Hero有三个属性 name,hp,damage
一个集合中放存放10个Hero,通过Collections.sort对这10个进行排序。
那么到底是hp小的放前面?还是damage小的放前面?Collections.sort也无法确定,所以要指定到底按照哪种属性进行排序。
这里就需要提供一个Comparator给定如何进行两个对象之间的大小比较。

Hero.java

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
package charactor;

public class Hero {
public String name;
public float hp;

public int damage;

public Hero() {

}

public Hero(String name) {

this.name = name;
}

public String toString() {
return "Hero [name=" + name + ", hp=" + hp + ", damage=" + damage + "]\r\n";
}

public Hero(String name, int hp, int damage) {
this.name = name;
this.hp = hp;
this.damage = damage;
}

}

TestCollection.java

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
package collection;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Random;

import charactor.Hero;

public class TestCollection {
public static void main(String[] args) {
Random r =new Random();
List<Hero> heros = new ArrayList<Hero>();

for (int i = 0; i < 10; i++) {
//通过随机值实例化hero的hp和damage
heros.add(new Hero("hero "+ i, r.nextInt(100), r.nextInt(100)));
}
System.out.println("初始化后的集合:");
System.out.println(heros);

//直接调用sort会出现编译错误,因为Hero有各种属性
//到底按照哪种属性进行比较,Collections也不知道,不确定,所以没法排
//Collections.sort(heros);

//引入Comparator,指定比较的算法
Comparator<Hero> c = new Comparator<Hero>() {
@Override
public int compare(Hero h1, Hero h2) {
//按照hp进行排序
if(h1.hp>=h2.hp)
return 1; //正数表示h1比h2要大
else
return -1;
}
};
Collections.sort(heros,c);
System.out.println("按照血量排序后的集合:");
System.out.println(heros);
}
}

Comparable

使Hero类实现Comparable接口
在类里面提供比较算法
Collections.sort就有足够的信息进行排序了,也无需额外提供比较器Comparator

注: 如果返回-1, 就表示当前的更小,否则就是更大。

Hero.java

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
package charactor;

public class Hero implements Comparable<Hero>{
public String name;
public float hp;

public int damage;

public Hero(){

}

public Hero(String name) {
this.name =name;

}

//初始化name,hp,damage的构造方法
public Hero(String name,float hp, int damage) {
this.name =name;
this.hp = hp;
this.damage = damage;
}

@Override
public int compareTo(Hero anotherHero) {
if(damage<anotherHero.damage)
return 1;
else
return -1;
}

@Override
public String toString() {
return "Hero [name=" + name + ", hp=" + hp + ", damage=" + damage + "]\r\n";
}

}

TestCollection.java

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
package collection;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Random;

import charactor.Hero;

public class TestCollection {
public static void main(String[] args) {
Random r =new Random();
List<Hero> heros = new ArrayList<Hero>();

for (int i = 0; i < 10; i++) {
//通过随机值实例化hero的hp和damage
heros.add(new Hero("hero "+ i, r.nextInt(100), r.nextInt(100)));
}

System.out.println("初始化后的集合");
System.out.println(heros);

//Hero类实现了接口Comparable,即自带比较信息。
//Collections直接进行排序,无需额外的Comparator
Collections.sort(heros);
System.out.println("按照伤害高低排序后的集合");
System.out.println(heros);

}
}

其他 - 聚合操作

JDK8之后,引入了对集合的聚合操作,可以非常容易的遍历,筛选,比较集合中的元素。

像这样:

1
2
3
4
5
6
7
8
 
String name =heros
.stream()
.sorted((h1,h2)->h1.hp>h2.hp?-1:1)
.skip(2)
.map(h->h.getName())
.findFirst()
.get();

但是要用好聚合,必须先掌握Lambda表达式。

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
package lambda;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Random;

import charactor.Hero;

public class TestAggregate {

public static void main(String[] args) {
Random r = new Random();
List<Hero> heros = new ArrayList<Hero>();
for (int i = 0; i < 10; i++) {
heros.add(new Hero("hero " + i, r.nextInt(1000), r.nextInt(100)));
}

System.out.println("初始化集合后的数据 (最后一个数据重复):");
System.out.println(heros);

//传统方式
Collections.sort(heros,new Comparator<Hero>() {
@Override
public int compare(Hero o1, Hero o2) {
return (int) (o2.hp-o1.hp);
}
});

Hero hero = heros.get(2);
System.out.println("通过传统方式找出来的hp第三高的英雄名称是:" + hero.name);

//聚合方式
String name =heros
.stream()
.sorted((h1,h2)->h1.hp>h2.hp?-1:1)
.skip(2)
.map(h->h.getName())
.findFirst()
.get();

System.out.println("通过聚合操作找出来的hp第三高的英雄名称是:" + name);

}
}

泛型

集合中的泛型

不使用泛型

不使用泛型带来的问题
ADHero(物理攻击英雄),APHero(魔法攻击英雄)都是Hero的子类。ArrayList 默认接受Object类型的对象,所以所有对象都可以放进ArrayList中。
即:get(0) 返回的类型是Object。
接着,需要进行强制转换才可以得到APHero类型或者ADHero类型。
如果软件开发人员记忆比较好,能记得哪个是哪个,还是可以的。 但是开发人员会犯错误,比如第20行,会记错,把第0个对象转换为ADHero,这样就会出现类型转换异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package generic;

import java.util.ArrayList;

import charactor.ADHero;
import charactor.APHero;

public class TestGeneric {

public static void main(String[] args) {

ArrayList heros = new ArrayList();

heros.add(new APHero());
heros.add(new ADHero());

APHero apHero = (APHero) heros.get(0);
ADHero adHero = (ADHero) heros.get(1);

ADHero adHero2 = (ADHero) heros.get(0);
}
}

使用泛型

使用泛型的好处:
泛型的用法是在容器后面添加
Type可以是类,抽象类,接口
泛型表示这种容器,只能存放APHero,ADHero就放不进去了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package generic;

import java.util.ArrayList;

import charactor.APHero;

public class TestGeneric {

public static void main(String[] args) {
ArrayList<APHero> heros = new ArrayList<APHero>();

//只有APHero可以放进去
heros.add(new APHero());

//ADHero甚至放不进去
//heros.add(new ADHero());

//获取的时候也不需要进行转型,因为取出来一定是APHero
APHero apHero = heros.get(0);

}
}

子类对象

假设容器的泛型是Hero, 那么Hero的子类APHero, ADHero都可以放进去,和Hero无关的类型Item还是放不进去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package generic;

import java.util.ArrayList;

import property.Item;

import charactor.ADHero;
import charactor.APHero;
import charactor.Hero;

public class TestGeneric {

public static void main(String[] args) {
ArrayList<Hero> heros = new ArrayList<Hero>();

//只有作为Hero的子类可以放进去
heros.add(new APHero());
heros.add(new ADHero());

//和Hero无关的类型Item还是放不进去
//heros.add(new Item());

}
}

泛型的简写

为了不使编译器出现警告,需要前后都使用泛型,像这样:

ArrayList<Hero> heros = new ArrayList<Hero>();

不过JDK7提供了一个可以略微减少代码量的泛型简写方式

ArrayList<Hero> heros2 = new ArrayList<>();

后面的泛型可以用<>来代替,聊胜于无吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package generic;

import java.util.ArrayList;

import charactor.Hero;

public class TestGeneric {

public static void main(String[] args) {
ArrayList<Hero> heros = new ArrayList<Hero>();
//后面可以只用<>
ArrayList<Hero> heros2 = new ArrayList<>();

}
}

支持泛型的类

不支持泛型的Stack

以Stack栈为例子,如果不使用泛型

  • 当需要一个只能放Hero的栈的时候,就需要设计一个HeroStack
  • 当需要一个只能放Item的栈的时候,就需要一个ItemStack

HeroStack.java

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
package generic;

import java.util.LinkedList;

import charactor.Hero;

public class HeroStack {

LinkedList<Hero> heros = new LinkedList<Hero>();

public void push(Hero h) {
heros.addLast(h);
}

public Hero pull() {
return heros.removeLast();
}

public Hero peek() {
return heros.getLast();
}

public static void main(String[] args) {

HeroStack heroStack = new HeroStack();
for (int i = 0; i < 5; i++) {
Hero h = new Hero("hero name " + i);
System.out.println("压入 hero:" + h);
heroStack.push(h);
}
for (int i = 0; i < 5; i++) {
Hero h =heroStack.pull();
System.out.println("弹出 hero" + h);
}
}

}

ItemStack.java

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
package generic;

import java.util.LinkedList;

import property.Item;

public class ItemStack {

LinkedList<Item> Items = new LinkedList<Item>();

public void push(Item h) {
Items.addLast(h);
}

public Item pull() {
return Items.removeLast();
}

public Item peek() {
return Items.getLast();
}

public static void main(String[] args) {

ItemStack ItemStack = new ItemStack();
for (int i = 0; i < 5; i++) {
Item item = new Item("Item name " + i);
System.out.println("压入 Item:" + item);
ItemStack.push(item);
}
for (int i = 0; i < 5; i++) {
Item item =ItemStack.pull();
System.out.println("弹出 Item" + item);
}
}

}

支持泛型的Stack

设计一个支持泛型的栈MyStack
设计这个类的时候,在类的声明上,加上一个,表示该类支持泛型。
T是type的缩写,也可以使用任何其他的合法的变量,比如A,B,X都可以,但是一般约定成俗使用T,代表类型。

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
package generic;

import java.util.HashMap;
import java.util.LinkedList;

import charactor.Hero;
import property.Item;

public class MyStack<T> {

LinkedList<T> values = new LinkedList<T>();

public void push(T t) {
values.addLast(t);
}

public T pull() {
return values.removeLast();
}

public T peek() {
return values.getLast();
}

public static void main(String[] args) {
//在声明这个Stack的时候,使用泛型<Hero>就表示该Stack只能放Hero
MyStack<Hero> heroStack = new MyStack<>();
heroStack.push(new Hero());
//不能放Item
heroStack.push(new Item());

//在声明这个Stack的时候,使用泛型<Item>就表示该Stack只能放Item
MyStack<Item> itemStack = new MyStack<>();
itemStack.push(new Item());
//不能放Hero
itemStack.push(new Hero());
}

}

通配符

? extends

ArrayList heroList<? extends Hero> 表示这是一个Hero泛型或者其子类泛型
heroList 的泛型可能是Hero
heroList 的泛型可能是APHero
heroList 的泛型可能是ADHero
所以 可以确凿的是,从heroList取出来的对象,一定是可以转型成Hero的

但是,不能往里面放东西,因为
放APHero就不满足
放ADHero又不满足

Alt text

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
package generic;

import java.util.ArrayList;

import charactor.ADHero;
import charactor.APHero;
import charactor.Hero;

public class TestGeneric {

public static void main(String[] args) {

ArrayList<APHero> apHeroList = new ArrayList<APHero>();
apHeroList.add(new APHero());

ArrayList<? extends Hero> heroList = apHeroList;

//? extends Hero 表示这是一个Hero泛型的子类泛型

//heroList 的泛型可以是Hero
//heroList 的泛型可以使APHero
//heroList 的泛型可以使ADHero

//可以确凿的是,从heroList取出来的对象,一定是可以转型成Hero的

Hero h= heroList.get(0);

//但是,不能往里面放东西
heroList.add(new ADHero()); //编译错误,因为heroList的泛型 有可能是APHero

}

}

? super

ArrayList heroList<? super Hero> 表示这是一个Hero泛型或者其父类泛型
heroList的泛型可能是Hero
heroList的泛型可能是Object

可以往里面插入Hero以及Hero的子类
但是取出来有风险,因为不确定取出来是Hero还是Object

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
package generic;

import java.util.ArrayList;

import charactor.ADHero;
import charactor.APHero;
import charactor.Hero;

public class TestGeneric {
public static void main(String[] args) {

ArrayList<? super Hero> heroList = new ArrayList<Object>();

//? super Hero 表示 heroList的泛型是Hero或者其父类泛型

//heroList 的泛型可以是Hero
//heroList 的泛型可以是Object

//所以就可以插入Hero
heroList.add(new Hero());
//也可以插入Hero的子类
heroList.add(new APHero());
heroList.add(new ADHero());

//但是,不能从里面取数据出来,因为其泛型可能是Object,而Object是强转Hero会失败
Hero h= heroList.get(0);

}

}

泛型通配符 ?

泛型通配符? 代表任意泛型
既然?代表任意泛型,那么换句话说,这个容器什么泛型都有可能

所以只能以Object的形式取出来
并且不能往里面放对象,因为不知道到底是一个什么泛型的容器

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
package generic;

import java.util.ArrayList;

import property.Item;
import charactor.APHero;
import charactor.Hero;

public class TestGeneric {

public static void main(String[] args) {

ArrayList<APHero> apHeroList = new ArrayList<APHero>();

//?泛型通配符,表示任意泛型
ArrayList<?> generalList = apHeroList;

//?的缺陷1: 既然?代表任意泛型,那么换句话说,你就不知道这个容器里面是什么类型
//所以只能以Object的形式取出来
Object o = generalList.get(0);

//?的缺陷2: 既然?代表任意泛型,那么既有可能是Hero,也有可能是Item
//所以,放哪种对象进去,都有风险,结果就什么什么类型的对象,都不能放进去
generalList.add(new Item()); //编译错误 因为?代表任意泛型,很有可能不是Item
generalList.add(new Hero()); //编译错误 因为?代表任意泛型,很有可能不是Hero
generalList.add(new APHero()); //编译错误 因为?代表任意泛型,很有可能不是APHero

}
}

泛型通配符总结

  • 如果希望只取出,不插入,就使用? extends Hero
  • 如果希望只插入,不取出,就使用? super Hero
  • 如果希望,又能插入,又能取出,就不要用通配符 ?

子类泛型 与 父类泛型 的转换

对象转型

根据面向对象学习的知识,子类转父类 是一定可以成功的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package generic;

import charactor.ADHero;
import charactor.Hero;

public class TestGeneric {

public static void main(String[] args) {

Hero h = new Hero();
ADHero ad = new ADHero();
//子类转父类
h = ad;
}

}

子类泛型 不能转 父类泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package generic;

import java.util.ArrayList;

import charactor.ADHero;
import charactor.APHero;
import charactor.Hero;

public class TestGeneric {

public static void main(String[] args) {
ArrayList<Hero> hs =new ArrayList<>();
ArrayList<ADHero> adhs =new ArrayList<>();

//假设能转换成功
hs = adhs;

//作为Hero泛型的hs,是可以向其中加入APHero的
//但是hs这个引用,实际上是指向的一个ADHero泛型的容器
//如果能加进去,就变成了ADHero泛型的容器里放进了APHero,这就矛盾了
hs.add(new APHero());
}

}

父类泛型 也不能转换为 子类泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package generic;

import java.util.ArrayList;

import charactor.ADHero;
import charactor.Hero;

public class TestGeneric {

public static void main(String[] args) {
ArrayList<Hero> hs =new ArrayList<>();
ArrayList<ADHero> adhs =new ArrayList<>();

//假设能成功
adhs = hs;

//这个时候adhs实际上指向的是泛型是Hero的容器,而这个容器里可能放的是一个APHero
//而根据泛型,直接取出来就转型成了ADHero
//所以就变成了APHero转型成ADHero,这是矛盾的。
ADHero ad =adhs.get(0);

}

}

Lambda

普通方法

使用一个普通方法,在for循环遍历中进行条件判断,筛选出满足条件的数据

hp>100 && damage<50

Hero.java

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
package charactor;

public class Hero implements Comparable<Hero>{
public String name;
public float hp;

public int damage;

public Hero(){

}

public Hero(String name) {
this.name =name;

}

//初始化name,hp,damage的构造方法
public Hero(String name,float hp, int damage) {
this.name =name;
this.hp = hp;
this.damage = damage;
}

@Override
public int compareTo(Hero anotherHero) {
if(damage<anotherHero.damage)
return 1;
else
return -1;
}

@Override
public String toString() {
return "Hero [name=" + name + ", hp=" + hp + ", damage=" + damage + "]\r\n";
}

}

TestLambda.java

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
package lambda;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import charactor.Hero;

public class TestLambda {
public static void main(String[] args) {
Random r = new Random();
List<Hero> heros = new ArrayList<Hero>();
for (int i = 0; i < 10; i++) {
heros.add(new Hero("hero " + i, r.nextInt(1000), r.nextInt(100)));
}
System.out.println("初始化后的集合:");
System.out.println(heros);
System.out.println("筛选出 hp>100 && damange<50的英雄");
filter(heros);
}

private static void filter(List<Hero> heros) {
for (Hero hero : heros) {
if(hero.hp>100 && hero.damage<50)
System.out.print(hero);
}
}

}

匿名类方式

首先准备一个接口HeroChecker,提供一个test(Hero)方法
然后通过匿名类的方式,实现这个接口

1
2
3
4
5
HeroChecker checker = new HeroChecker() {
public boolean test(Hero h) {
return (h.hp>100 && h.damage<50);
}
};

接着调用filter,传递这个checker进去进行判断,这种方式就很像通过Collections.sort在对一个Hero集合排序,需要传一个Comparator的匿名类对象进去一样。

HeroChecker.java

1
2
3
4
5
6
7
package lambda;

import charactor.Hero;

public interface HeroChecker {
public boolean test(Hero h);
}

TestLambda.java

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
package lambda;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import charactor.Hero;

public class TestLambda {
public static void main(String[] args) {
Random r = new Random();
List<Hero> heros = new ArrayList<Hero>();
for (int i = 0; i < 5; i++) {
heros.add(new Hero("hero " + i, r.nextInt(1000), r.nextInt(100)));
}
System.out.println("初始化后的集合:");
System.out.println(heros);
System.out.println("使用匿名类的方式,筛选出 hp>100 && damange<50的英雄");
HeroChecker checker = new HeroChecker() {
@Override
public boolean test(Hero h) {
return (h.hp>100 && h.damage<50);
}
};

filter(heros,checker);
}

private static void filter(List<Hero> heros,HeroChecker checker) {
for (Hero hero : heros) {
if(checker.test(hero))
System.out.print(hero);
}
}

}

Lambda方式

使用Lambda方式筛选出数据

filter(heros,(h)->h.hp>100 && h.damage<50);

同样是调用filter方法,从上一步的传递匿名类对象,变成了传递一个Lambda表达式进去

h->h.hp>100 && h.damage<50

咋一看Lambda表达式似乎不好理解,其实很简单,下一步讲解如何从一个匿名类一点点演变成Lambda表达式。

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
package lambda;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import charactor.Hero;

public class TestLamdba {
public static void main(String[] args) {
Random r = new Random();
List<Hero> heros = new ArrayList<Hero>();
for (int i = 0; i < 5; i++) {
heros.add(new Hero("hero " + i, r.nextInt(1000), r.nextInt(100)));
}
System.out.println("初始化后的集合:");
System.out.println(heros);
System.out.println("使用Lamdba的方式,筛选出 hp>100 && damange<50的英雄");
filter(heros,h->h.hp>100 && h.damage<50);
}

private static void filter(List<Hero> heros,HeroChecker checker) {
for (Hero hero : heros) {
if(checker.test(hero))
System.out.print(hero);
}
}

}

从匿名类演变成Lambda表达式

Lambda表达式可以看成是匿名类一点点演变过来

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
package lambda;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import charactor.Hero;

public class TestLamdba {
public static void main(String[] args) {
Random r = new Random();
List<Hero> heros = new ArrayList<Hero>();
for (int i = 0; i < 5; i++) {
heros.add(new Hero("hero " + i, r.nextInt(1000), r.nextInt(100)));
}
System.out.println("初始化后的集合:");
System.out.println(heros);
System.out.println("使用匿名类的方式,筛选出 hp>100 && damange<50的英雄");
// 1.匿名类的正常写法
HeroChecker c1 = new HeroChecker() {
@Override
public boolean test(Hero h) {
return (h.hp > 100 && h.damage < 50);
}
};
// 2.把new HeroCheker,方法名,方法返回类型信息去掉
// 只保留方法参数和方法体
// 参数和方法体之间加上符号 ->
HeroChecker c2 = (Hero h) -> {
return h.hp > 100 && h.damage < 50;
};

// 3.把return和{}去掉
HeroChecker c3 = (Hero h) -> h.hp > 100 && h.damage < 50;

// 4.把参数类型和圆括号去掉
HeroChecker c4 = h -> h.hp > 100 && h.damage < 50;

// 5.把c4作为参数传递进去
filter(heros, c4);

// 6.直接把表达式传递进去
filter(heros, h -> h.hp > 100 && h.damage < 50);
}

private static void filter(List<Hero> heros, HeroChecker checker) {
for (Hero hero : heros) {
if (checker.test(hero))
System.out.print(hero);
}
}

}

匿名方法

与匿名类 概念相比较,
Lambda 其实就是匿名方法,这是一种把方法作为参数进行传递的编程思想。

虽然代码是这么写

filter(heros, h -> h.hp > 100 && h.damage < 50);

但是,Java会在背后,悄悄的,把这些都还原成匿名类方式。
引入Lambda表达式,会使得代码更加紧凑,而不是各种接口和匿名类到处飞。

Lambda的弊端

Lambda表达式虽然带来了代码的简洁,但是也有其局限性。

  1. 可读性差,与啰嗦的但是清晰的匿名类代码结构比较起来,Lambda表达式一旦变得比较长,就难以理解
  2. 不便于调试,很难在Lambda表达式中增加调试信息,比如日志
  3. 版本支持,Lambda表达式在JDK8版本中才开始支持,如果系统使用的是以前的版本,考虑系统的稳定性等原因,而不愿意升级,那么就无法使用。

Lambda比较适合用在简短的业务代码中,并不适合用在复杂的系统中,会加大维护成本。

方法引用

引用静态方法

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
package lambda;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import charactor.Hero;

public class TestLambda {
public static void main(String[] args) {
Random r = new Random();
List<Hero> heros = new ArrayList<Hero>();
for (int i = 0; i < 5; i++) {
heros.add(new Hero("hero " + i, r.nextInt(1000), r.nextInt(100)));
}
System.out.println("初始化后的集合:");
System.out.println(heros);

HeroChecker c = new HeroChecker() {
public boolean test(Hero h) {
return h.hp>100 && h.damage<50;
}
};

System.out.println("使用匿名类过滤");
filter(heros, c);
System.out.println("使用Lambda表达式");
filter(heros, h->h.hp>100 && h.damage<50);
System.out.println("在Lambda表达式中使用静态方法");
filter(heros, h -> TestLambda.testHero(h) );
System.out.println("直接引用静态方法");
filter(heros, TestLambda::testHero);
}

public static boolean testHero(Hero h) {
return h.hp>100 && h.damage<50;
}

private static void filter(List<Hero> heros, HeroChecker checker) {
for (Hero hero : heros) {
if (checker.test(hero))
System.out.print(hero);
}
}

}

引用对象方法

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
package lambda;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import charactor.Hero;

public class TestLambda {
public static void main(String[] args) {
Random r = new Random();
List<Hero> heros = new ArrayList<Hero>();
for (int i = 0; i < 5; i++) {
heros.add(new Hero("hero " + i, r.nextInt(1000), r.nextInt(100)));
}
System.out.println("初始化后的集合:");
System.out.println(heros);

System.out.println("使用引用对象方法 的过滤结果:");
//使用类的对象方法
TestLambda testLambda = new TestLambda();
filter(heros, testLambda::testHero);
}

public boolean testHero(Hero h) {
return h.hp>100 && h.damage<50;
}

private static void filter(List<Hero> heros, HeroChecker checker) {
for (Hero hero : heros) {
if (checker.test(hero))
System.out.print(hero);
}
}

}

引用容器中的对象的方法

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
package lambda;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import charactor.Hero;

public class TestLambda {
public static void main(String[] args) {
Random r = new Random();
List<Hero> heros = new ArrayList<Hero>();
for (int i = 0; i < 5; i++) {
heros.add(new Hero("hero " + i, r.nextInt(1000), r.nextInt(100)));
}
System.out.println("初始化后的集合:");
System.out.println(heros);

System.out.println("Lambda表达式:");
filter(heros,h-> h.hp>100 && h.damage<50 );

System.out.println("Lambda表达式中调用容器中的对象的matched方法:");
filter(heros,h-> h.matched() );

System.out.println("引用容器中对象的方法 之过滤结果:");
filter(heros, Hero::matched);
}

public boolean testHero(Hero h) {
return h.hp>100 && h.damage<50;
}

private static void filter(List<Hero> heros, HeroChecker checker) {
for (Hero hero : heros) {
if (checker.test(hero))
System.out.print(hero);
}
}

}

引用构造器

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
package lambda;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;

public class TestLambda {
public static void main(String[] args) {
Supplier<List> s = new Supplier<List>() {
public List get() {
return new ArrayList();
}
};

//匿名类
List list1 = getList(s);

//Lambda表达式
List list2 = getList(()->new ArrayList());

//引用构造器
List list3 = getList(ArrayList::new);

}

public static List getList(Supplier<List> s){
return s.get();
}

}

聚合操作

传统方式与聚合操作方式遍历数据

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
package lambda;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import charactor.Hero;

public class TestAggregate {

public static void main(String[] args) {
Random r = new Random();
List<Hero> heros = new ArrayList<Hero>();
for (int i = 0; i < 5; i++) {
heros.add(new Hero("hero " + i, r.nextInt(1000), r.nextInt(100)));
}

System.out.println("初始化后的集合:");
System.out.println(heros);
System.out.println("查询条件:hp>100 && damage<50");
System.out.println("通过传统操作方式找出满足条件的数据:");

for (Hero h : heros) {
if (h.hp > 100 && h.damage < 50)
System.out.println(h.name);
}

System.out.println("通过聚合操作方式找出满足条件的数据:");
heros
.stream()
.filter(h -> h.hp > 100 && h.damage < 50)
.forEach(h -> System.out.println(h.name));

}
}

Stream和管道的概念

要了解聚合操作,首先要建立Stream和管道的概念
Stream 和Collection结构化的数据不一样,Stream是一系列的元素,就像是生产线上的罐头一样,一串串的出来。

管道指的是一系列的聚合操作。
管道又分3个部分:

  • 管道源:在这个例子里,源是一个List
  • 中间操作: 每个中间操作,又会返回一个Stream,比如.filter()又返回一个Stream, 中间操作是“懒”操作,并不会真正进行遍历。
  • 结束操作:当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。 结束操作不会返回Stream,但是会返回int、float、String、 Collection或者像forEach,什么都不返回, 结束操作才进行真正的遍历行为,在遍历的时候,才会去进行中间操作的相关判断。

注: 这个Stream和I/O章节的InputStream,OutputStream是不一样的概念。

管道源

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
package lambda;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Random;

import charactor.Hero;

public class TestAggregate {

public static void main(String[] args) {
Random r = new Random();
List<Hero> heros = new ArrayList<Hero>();
for (int i = 0; i < 5; i++) {
heros.add(new Hero("hero " + i, r.nextInt(1000), r.nextInt(100)));
}
//管道源是集合
heros
.stream()
.forEach(h->System.out.println(h.name));

//管道源是数组
Hero hs[] = heros.toArray(new Hero[heros.size()]);
Arrays.stream(hs)
.forEach(h->System.out.println(h.name));

}
}

中间操作

每个中间操作,又会返回一个Stream,比如.filter()又返回一个Stream, 中间操作是“懒”操作,并不会真正进行遍历。

中间操作比较多,主要分两类:

  • 对元素进行筛选
  • 转换为其他形式的流

对元素进行筛选

  • filter 匹配
  • distinct 去除重复(根据equals判断)
  • sorted 自然排序
  • sorted(Comparator) 指定排序
  • limit 保留
  • skip 忽略

转换为其他形式的流

  • mapToDouble 转换为double的流
  • map 转换为任意类型的流

Hero.java

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
package charactor;

public class Hero implements Comparable<Hero>{
public String name;
public float hp;

public int damage;

public Hero(){

}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public float getHp() {
return hp;
}
public void setHp(float hp) {
this.hp = hp;
}
public int getDamage() {
return damage;
}
public void setDamage(int damage) {
this.damage = damage;
}
public Hero(String name) {
this.name =name;
}
//初始化name,hp,damage的构造方法
public Hero(String name,float hp, int damage) {
this.name =name;
this.hp = hp;
this.damage = damage;
}

@Override
public int compareTo(Hero anotherHero) {
if(damage<anotherHero.damage)
return 1;
else
return -1;
}

@Override
public String toString() {
return "Hero [name=" + name + ", hp=" + hp + ", damage=" + damage + "]\r\n";
}

}

TestAggregate.java

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
package lambda;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import charactor.Hero;

public class TestAggregate {

public static void main(String[] args) {
Random r = new Random();
List<Hero> heros = new ArrayList<Hero>();
for (int i = 0; i < 5; i++) {
heros.add(new Hero("hero " + i, r.nextInt(1000), r.nextInt(100)));
}
//制造一个重复数据
heros.add(heros.get(0));
System.out.println("初始化集合后的数据 (最后一个数据重复):");
System.out.println(heros);
System.out.println("满足条件hp>100&&damage<50的数据");

heros
.stream()
.filter(h->h.hp>100&&h.damage<50)
.forEach(h->System.out.print(h));

System.out.println("去除重复的数据,去除标准是看equals");
heros
.stream()
.distinct()
.forEach(h->System.out.print(h));
System.out.println("按照血量排序");
heros
.stream()
.sorted((h1,h2)->h1.hp>=h2.hp?1:-1)
.forEach(h->System.out.print(h));

System.out.println("保留3个");
heros
.stream()
.limit(3)
.forEach(h->System.out.print(h));

System.out.println("忽略前3个");
heros
.stream()
.skip(3)
.forEach(h->System.out.print(h));

System.out.println("转换为double的Stream");
heros
.stream()
.mapToDouble(Hero::getHp)
.forEach(h->System.out.println(h));

System.out.println("转换任意类型的Stream");
heros
.stream()
.map((h)-> h.name + " - " + h.hp + " - " + h.damage)
.forEach(h->System.out.println(h));

}
}

结束操作

当进行结束操作后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。 结束操作不会返回Stream,但是会返回int、float、String、 Collection或者像forEach,什么都不返回,。

结束操作才真正进行遍历行为,前面的中间操作也在这个时候,才真正的执行。

常见结束操作如下:

  • forEach() 遍历每个元素
  • toArray() 转换为数组
  • min(Comparator) 取最小的元素
  • max(Comparator) 取最大的元素
  • count() 总数
  • findFirst() 第一个元素
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
package lambda;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;

import org.omg.Messaging.SYNC_WITH_TRANSPORT;

import charactor.Hero;

public class TestAggregate {

public static void main(String[] args) {
Random r = new Random();
List<Hero> heros = new ArrayList<Hero>();
for (int i = 0; i < 5; i++) {
heros.add(new Hero("hero " + i, r.nextInt(1000), r.nextInt(100)));
}
System.out.println("遍历集合中的每个数据");
heros
.stream()
.forEach(h->System.out.print(h));
System.out.println("返回一个数组");
Object[] hs= heros
.stream()
.toArray();
System.out.println(Arrays.toString(hs));
System.out.println("返回伤害最低的那个英雄");
Hero minDamageHero =
heros
.stream()
.min((h1,h2)->h1.damage-h2.damage)
.get();
System.out.print(minDamageHero);
System.out.println("返回伤害最高的那个英雄");

Hero mxnDamageHero =
heros
.stream()
.max((h1,h2)->h1.damage-h2.damage)
.get();
System.out.print(mxnDamageHero);

System.out.println("流中数据的总数");
long count = heros
.stream()
.count();
System.out.println(count);

System.out.println("第一个英雄");
Hero firstHero =
heros
.stream()
.findFirst()
.get();

System.out.println(firstHero);

}
}

多线程

启动一个线程

多线程即在同一时间,可以做多件事情。

创建多线程有3种方式,分别是:

  • 继承线程类
  • 实现Runnable接口
  • 匿名类

线程概念

首先要理解进程(Processor)和线程(Thread)的区别

  • 进程:启动一个LOL.exe就叫一个进程。 接着又启动一个DOTA.exe,这叫两个进程。
  • 线程:线程是在进程内部同时做的事情,比如在LOL里,有很多事情要同时做,比如”盖伦” 击杀“提莫”,同时“赏金猎人”又在击杀“盲僧”,这就是由多线程来实现的。

此处代码演示的是不使用多线程的情况:
只有在盖伦杀掉提莫后,赏金猎人才开始杀盲僧。

Hero.java

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
package charactor;

import java.io.Serializable;

public class Hero{
public String name;
public float hp;

public int damage;

public void attackHero(Hero h) {
try {
//为了表示攻击需要时间,每次攻击暂停1000毫秒
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
h.hp-=damage;
System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n",name,h.name,h.name,h.hp);

if(h.isDead())
System.out.println(h.name +"死了!");
}

public boolean isDead() {
return 0>=hp?true:false;
}

}

TestThread.java

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
package multiplethread;

import charactor.Hero;

public class TestThread {

public static void main(String[] args) {

Hero gareen = new Hero();
gareen.name = "盖伦";
gareen.hp = 616;
gareen.damage = 50;

Hero teemo = new Hero();
teemo.name = "提莫";
teemo.hp = 300;
teemo.damage = 30;

Hero bh = new Hero();
bh.name = "赏金猎人";
bh.hp = 500;
bh.damage = 65;

Hero leesin = new Hero();
leesin.name = "盲僧";
leesin.hp = 455;
leesin.damage = 80;

//盖伦攻击提莫
while(!teemo.isDead()){
gareen.attackHero(teemo);
}

//赏金猎人攻击盲僧
while(!leesin.isDead()){
bh.attackHero(leesin);
}
}

}

创建多线程-继承线程类

使用多线程,就可以做到盖伦在攻击提莫的同时,赏金猎人也在攻击盲僧
设计一个类KillThread 继承Thread,并且重写run方法

启动线程办法: 实例化一个KillThread对象,并且调用其start方法
就可以观察到 赏金猎人攻击盲僧的同时,盖伦也在攻击提莫。

KillThread.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package multiplethread;

import charactor.Hero;

public class KillThread extends Thread{

private Hero h1;
private Hero h2;

public KillThread(Hero h1, Hero h2){
this.h1 = h1;
this.h2 = h2;
}

public void run(){
while(!h2.isDead()){
h1.attackHero(h2);
}
}
}

TestThread.java

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
package multiplethread;

import charactor.Hero;

public class TestThread {

public static void main(String[] args) {

Hero gareen = new Hero();
gareen.name = "盖伦";
gareen.hp = 616;
gareen.damage = 50;

Hero teemo = new Hero();
teemo.name = "提莫";
teemo.hp = 300;
teemo.damage = 30;

Hero bh = new Hero();
bh.name = "赏金猎人";
bh.hp = 500;
bh.damage = 65;

Hero leesin = new Hero();
leesin.name = "盲僧";
leesin.hp = 455;
leesin.damage = 80;

KillThread killThread1 = new KillThread(gareen,teemo);
killThread1.start();
KillThread killThread2 = new KillThread(bh,leesin);
killThread2.start();

}

}

创建多线程-实现Runnable接口

创建类Battle,实现Runnable接口
启动的时候,首先创建一个Battle对象,然后再根据该battle对象创建一个线程对象,并启动

1
2
Battle battle1 = new Battle(gareen,teemo);
new Thread(battle1).start();

battle1 对象实现了Runnable接口,所以有run方法,但是直接调用run方法,并不会启动一个新的线程。必须借助一个线程对象的start()方法,才会启动一个新的线程。

所以,在创建Thread对象的时候,把battle1作为构造方法的参数传递进去,这个线程启动的时候,就会去执行battle1.run()方法了。

Battle.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package multiplethread;

import charactor.Hero;

public class Battle implements Runnable{

private Hero h1;
private Hero h2;

public Battle(Hero h1, Hero h2){
this.h1 = h1;
this.h2 = h2;
}

public void run(){
while(!h2.isDead()){
h1.attackHero(h2);
}
}
}

TestThread.java

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
package multiplethread;

import charactor.Hero;

public class TestThread {

public static void main(String[] args) {

Hero gareen = new Hero();
gareen.name = "盖伦";
gareen.hp = 616;
gareen.damage = 50;

Hero teemo = new Hero();
teemo.name = "提莫";
teemo.hp = 300;
teemo.damage = 30;

Hero bh = new Hero();
bh.name = "赏金猎人";
bh.hp = 500;
bh.damage = 65;

Hero leesin = new Hero();
leesin.name = "盲僧";
leesin.hp = 455;
leesin.damage = 80;

Battle battle1 = new Battle(gareen,teemo);

new Thread(battle1).start();

Battle battle2 = new Battle(bh,leesin);
new Thread(battle2).start();

}

}

创建多线程-匿名类

使用匿名类,继承Thread,重写run方法,直接在run方法中写业务代码
匿名类的一个好处是可以很方便的访问外部的局部变量。
前提是外部的局部变量需要被声明为final。(JDK7以后就不需要了)

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
package multiplethread;

import charactor.Hero;

public class TestThread {

public static void main(String[] args) {

Hero gareen = new Hero();
gareen.name = "盖伦";
gareen.hp = 616;
gareen.damage = 50;

Hero teemo = new Hero();
teemo.name = "提莫";
teemo.hp = 300;
teemo.damage = 30;

Hero bh = new Hero();
bh.name = "赏金猎人";
bh.hp = 500;
bh.damage = 65;

Hero leesin = new Hero();
leesin.name = "盲僧";
leesin.hp = 455;
leesin.damage = 80;

//匿名类
Thread t1= new Thread(){
public void run(){
//匿名类中用到外部的局部变量teemo,必须把teemo声明为final
//但是在JDK7以后,就不是必须加final的了
while(!teemo.isDead()){
gareen.attackHero(teemo);
}
}
};

t1.start();

Thread t2= new Thread(){
public void run(){
while(!leesin.isDead()){
bh.attackHero(leesin);
}
}
};
t2.start();

}

}

创建多线程的三种方式

把上述3种方式再整理一下:

  1. 继承Thread类
  2. 实现Runnable接口
  3. 匿名类的方式

注: 启动线程是start()方法,run()并不能启动一个新的线程。

常见线程方法

当前线程暂停

Thread.sleep(1000); 表示当前线程暂停1000毫秒 ,其他线程不受影响
Thread.sleep(1000); 会抛出InterruptedException 中断异常,因为当前线程sleep的时候,有可能被停止,这时就会抛出 InterruptedException

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
package multiplethread;

public class TestThread {

public static void main(String[] args) {

Thread t1= new Thread(){
public void run(){
int seconds =0;
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.printf("已经玩了LOL %d 秒%n", seconds++);
}
}
};
t1.start();

}

}

加入到当前线程中

首先解释一下主线程的概念
所有进程,至少会有一个线程即主线程,即main方法开始执行,就会有一个看不见的主线程存在。

在42行执行t.join,即表明在主线程中加入该线程。
主线程会等待该线程结束完毕, 才会往下运行。

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
package multiplethread;

import charactor.Hero;

public class TestThread {

public static void main(String[] args) {

final Hero gareen = new Hero();
gareen.name = "盖伦";
gareen.hp = 616;
gareen.damage = 50;

final Hero teemo = new Hero();
teemo.name = "提莫";
teemo.hp = 300;
teemo.damage = 30;

final Hero bh = new Hero();
bh.name = "赏金猎人";
bh.hp = 500;
bh.damage = 65;

final Hero leesin = new Hero();
leesin.name = "盲僧";
leesin.hp = 455;
leesin.damage = 80;

Thread t1= new Thread(){
public void run(){
while(!teemo.isDead()){
gareen.attackHero(teemo);
}
}
};

t1.start();

//代码执行到这里,一直是main线程在运行
try {
//t1线程加入到main线程中来,只有t1线程运行结束,才会继续往下走
t1.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

Thread t2= new Thread(){
public void run(){
while(!leesin.isDead()){
bh.attackHero(leesin);
}
}
};
//会观察到盖伦把提莫杀掉后,才运行t2线程
t2.start();

}

}

线程优先级

当线程处于竞争关系的时候,优先级高的线程会有更大的几率获得CPU资源
为了演示该效果,要把暂停时间去掉,多条线程各自会尽力去占有CPU资源
同时把英雄的血量增加100倍,攻击减低到1,才有足够的时间观察到优先级的演示
如图可见,线程1的优先级是MAX_PRIORITY,所以它争取到了更多的CPU资源执行代码

Hero.java

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
package charactor;

import java.io.Serializable;

public class Hero{
public String name;
public float hp;

public int damage;

public void attackHero(Hero h) {
//把暂停时间去掉,多条线程各自会尽力去占有CPU资源
//线程的优先级效果才可以看得出来
// try {
//
// Thread.sleep(0);
// } catch (InterruptedException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
h.hp-=damage;
System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n",name,h.name,h.name,h.hp);

if(h.isDead())
System.out.println(h.name +"死了!");
}

public boolean isDead() {
return 0>=hp?true:false;
}

}

TestThread.java

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
package multiplethread;

import charactor.Hero;

public class TestThread {

public static void main(String[] args) {

final Hero gareen = new Hero();
gareen.name = "盖伦";
gareen.hp = 6160;
gareen.damage = 1;

final Hero teemo = new Hero();
teemo.name = "提莫";
teemo.hp = 3000;
teemo.damage = 1;

final Hero bh = new Hero();
bh.name = "赏金猎人";
bh.hp = 5000;
bh.damage = 1;

final Hero leesin = new Hero();
leesin.name = "盲僧";
leesin.hp = 4505;
leesin.damage = 1;

Thread t1= new Thread(){
public void run(){

while(!teemo.isDead()){
gareen.attackHero(teemo);
}
}
};

Thread t2= new Thread(){
public void run(){
while(!leesin.isDead()){
bh.attackHero(leesin);
}
}
};

t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);
t1.start();
t2.start();

}

}

临时暂停

当前线程,临时暂停,使得其他线程可以有更多的机会占用CPU资源。

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
package multiplethread;

import charactor.Hero;

public class TestThread {

public static void main(String[] args) {

final Hero gareen = new Hero();
gareen.name = "盖伦";
gareen.hp = 61600;
gareen.damage = 1;

final Hero teemo = new Hero();
teemo.name = "提莫";
teemo.hp = 30000;
teemo.damage = 1;

final Hero bh = new Hero();
bh.name = "赏金猎人";
bh.hp = 50000;
bh.damage = 1;

final Hero leesin = new Hero();
leesin.name = "盲僧";
leesin.hp = 45050;
leesin.damage = 1;

Thread t1= new Thread(){
public void run(){

while(!teemo.isDead()){
gareen.attackHero(teemo);
}
}
};

Thread t2= new Thread(){
public void run(){
while(!leesin.isDead()){
//临时暂停,使得t1可以占用CPU资源
Thread.yield();

bh.attackHero(leesin);
}
}
};

t1.setPriority(5);
t2.setPriority(5);
t1.start();
t2.start();

}

}

守护线程

守护线程的概念是: 当一个进程里,所有的线程都是守护线程的时候,结束当前进程。

就好像一个公司有销售部,生产部这些和业务挂钩的部门。
除此之外,还有后勤,行政等这些支持部门。

如果一家公司销售部,生产部都解散了,那么只剩下后勤和行政,那么这家公司也可以解散了。

守护线程就相当于那些支持部门,如果一个进程只剩下守护线程,那么进程就会自动结束。

守护线程通常会被用来做日志,性能统计等工作。

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
package multiplethread;

public class TestThread {

public static void main(String[] args) {

Thread t1= new Thread(){
public void run(){
int seconds =0;

while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.printf("已经玩了LOL %d 秒%n", seconds++);

}
}
};
t1.setDaemon(true);
t1.start();

}

}

同步

多线程的同步问题指的是多个线程同时修改一个数据的时候,可能导致的问题。
多线程的问题,又叫Concurrency 问题。

演示同步问题

假设盖伦有10000滴血,并且在基地里,同时又被对方多个英雄攻击
就是有多个线程在减少盖伦的hp
同时又有多个线程在恢复盖伦的hp
假设线程的数量是一样的,并且每次改变的值都是1,那么所有线程结束后,盖伦应该还是10000滴血。

但是。。。

注意: 不是每一次运行都会看到错误的数据产生,多运行几次,或者增加运行的次数。

Hero.java

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
package charactor;

public class Hero{
public String name;
public float hp;

public int damage;

//回血
public void recover(){
hp=hp+1;
}

//掉血
public void hurt(){
hp=hp-1;
}

public void attackHero(Hero h) {
h.hp-=damage;
System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n",name,h.name,h.name,h.hp);
if(h.isDead())
System.out.println(h.name +"死了!");
}

public boolean isDead() {
return 0>=hp?true:false;
}

}

TestThread.java

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
package multiplethread;

import charactor.Hero;

public class TestThread {

public static void main(String[] args) {

final Hero gareen = new Hero();
gareen.name = "盖伦";
gareen.hp = 10000;

System.out.printf("盖伦的初始血量是 %.0f%n", gareen.hp);

//多线程同步问题指的是多个线程同时修改一个数据的时候,导致的问题

//假设盖伦有10000滴血,并且在基地里,同时又被对方多个英雄攻击

//用JAVA代码来表示,就是有多个线程在减少盖伦的hp
//同时又有多个线程在恢复盖伦的hp

//n个线程增加盖伦的hp

int n = 10000;

Thread[] addThreads = new Thread[n];
Thread[] reduceThreads = new Thread[n];

for (int i = 0; i < n; i++) {
Thread t = new Thread(){
public void run(){
gareen.recover();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
t.start();
addThreads[i] = t;

}

//n个线程减少盖伦的hp
for (int i = 0; i < n; i++) {
Thread t = new Thread(){
public void run(){
gareen.hurt();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
t.start();
reduceThreads[i] = t;
}

//等待所有增加线程结束
for (Thread t : addThreads) {
try {
t.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//等待所有减少线程结束
for (Thread t : reduceThreads) {
try {
t.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

//代码执行到这里,所有增加和减少线程都结束了

//增加和减少线程的数量是一样的,每次都增加,减少1.
//那么所有线程都结束后,盖伦的hp应该还是初始值

//但是事实上观察到的是:

System.out.printf("%d个增加线程和%d个减少线程结束后%n盖伦的血量变成了 %.0f%n", n,n,gareen.hp);

}

}

分析同步问题产生的原因

  1. 假设增加线程先进入,得到的hp是10000
  2. 进行增加运算
  3. 正在做增加运算的时候,还没有来得及修改hp的值,减少线程来了
  4. 减少线程得到的hp的值也是10000
  5. 减少线程进行减少运算
  6. 增加线程运算结束,得到值10001,并把这个值赋予hp
  7. 减少线程也运算结束,得到值9999,并把这个值赋予hp

hp,最后的值就是9999

虽然经历了两个线程各自增减了一次,本来期望还是原值10000,但是却得到了一个9999
这个时候的值9999是一个错误的值,在业务上又叫做脏数据

解决思路

总体解决思路是: 在增加线程访问hp期间,其他线程不可以访问hp

  1. 增加线程获取到hp的值,并进行运算
  2. 在运算期间,减少线程试图来获取hp的值,但是不被允许
  3. 增加线程运算结束,并成功修改hp的值为10001
  4. 减少线程,在增加线程做完后,才能访问hp的值,即10001
  5. 减少线程运算,并得到新的值10000

synchronized 同步对象概念

解决上述问题之前,先理解synchronized关键字的意义
如下代码:

1
2
3
4
Object someObject =new Object();
synchronized (someObject){
//此处的代码只有占有了someObject后才可以执行
}

synchronized表示当前线程,独占对象 someObject
当前线程独占了对象someObject,如果有其他线程试图占有对象someObject,就会等待,直到当前线程释放对someObject的占用。

someObject 又叫同步对象,所有的对象,都可以作为同步对象
为了达到同步的效果,必须使用同一个同步对象

释放同步对象的方式: synchronized 块自然结束,或者有异常抛出

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
package multiplethread;

import java.text.SimpleDateFormat;
import java.util.Date;

public class TestThread {

public static String now(){
return new SimpleDateFormat("HH:mm:ss").format(new Date());
}

public static void main(String[] args) {
final Object someObject = new Object();

Thread t1 = new Thread(){
public void run(){
try {
System.out.println( now()+" t1 线程已经运行");
System.out.println( now()+this.getName()+ " 试图占有对象:someObject");
synchronized (someObject) {

System.out.println( now()+this.getName()+ " 占有对象:someObject");
Thread.sleep(5000);
System.out.println( now()+this.getName()+ " 释放对象:someObject");
}
System.out.println(now()+" t1 线程结束");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
t1.setName(" t1");
t1.start();
Thread t2 = new Thread(){

public void run(){
try {
System.out.println( now()+" t2 线程已经运行");
System.out.println( now()+this.getName()+ " 试图占有对象:someObject");
synchronized (someObject) {
System.out.println( now()+this.getName()+ " 占有对象:someObject");
Thread.sleep(5000);
System.out.println( now()+this.getName()+ " 释放对象:someObject");
}
System.out.println(now()+" t2 线程结束");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
t2.setName(" t2");
t2.start();
}

}

使用synchronized 解决同步问题

所有需要修改hp的地方,有要建立在占有someObject的基础上
而对象 someObject在同一时间,只能被一个线程占有。 间接地,导致同一时间,hp只能被一个线程修改

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
package multiplethread;

import java.awt.GradientPaint;

import charactor.Hero;

public class TestThread {

public static void main(String[] args) {

final Object someObject = new Object();

final Hero gareen = new Hero();
gareen.name = "盖伦";
gareen.hp = 10000;

int n = 10000;

Thread[] addThreads = new Thread[n];
Thread[] reduceThreads = new Thread[n];

for (int i = 0; i < n; i++) {
Thread t = new Thread(){
public void run(){

//任何线程要修改hp的值,必须先占用someObject
synchronized (someObject) {
gareen.recover();
}

try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
t.start();
addThreads[i] = t;

}

for (int i = 0; i < n; i++) {
Thread t = new Thread(){
public void run(){
//任何线程要修改hp的值,必须先占用someObject
synchronized (someObject) {
gareen.hurt();
}

try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
t.start();
reduceThreads[i] = t;
}

for (Thread t : addThreads) {
try {
t.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
for (Thread t : reduceThreads) {
try {
t.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

System.out.printf("%d个增加线程和%d个减少线程结束后%n盖伦的血量是 %.0f%n", n,n,gareen.hp);

}

}

使用hero对象作为同步对象

既然任意对象都可以用来作为同步对象,而所有的线程访问的都是同一个hero对象,索性就使用gareen来作为同步对象

进一步的,对于Hero的hurt方法,加上:

1
2
synchronized (this) {
}

表示当前对象为同步对象,即也是gareen为同步对象。

Hero.java

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
package charactor;

public class Hero{
public String name;
public float hp;

public int damage;

//回血
public void recover(){
hp=hp+1;
}

//掉血
public void hurt(){
//使用this作为同步对象
synchronized (this) {
hp=hp-1;
}
}

public void attackHero(Hero h) {
h.hp-=damage;
System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n",name,h.name,h.name,h.hp);
if(h.isDead())
System.out.println(h.name +"死了!");
}

public boolean isDead() {
return 0>=hp?true:false;
}

}

TestThread.java

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
package multiplethread;

import java.awt.GradientPaint;

import charactor.Hero;

public class TestThread {

public static void main(String[] args) {

final Hero gareen = new Hero();
gareen.name = "盖伦";
gareen.hp = 10000;

int n = 10000;

Thread[] addThreads = new Thread[n];
Thread[] reduceThreads = new Thread[n];

for (int i = 0; i < n; i++) {
Thread t = new Thread(){
public void run(){

//使用gareen作为synchronized
synchronized (gareen) {
gareen.recover();
}

try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
t.start();
addThreads[i] = t;

}

for (int i = 0; i < n; i++) {
Thread t = new Thread(){
public void run(){
//使用gareen作为synchronized
//在方法hurt中有synchronized(this)
gareen.hurt();

try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
t.start();
reduceThreads[i] = t;
}

for (Thread t : addThreads) {
try {
t.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
for (Thread t : reduceThreads) {
try {
t.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

System.out.printf("%d个增加线程和%d个减少线程结束后%n盖伦的血量是 %.0f%n", n,n,gareen.hp);

}

}

在方法前,加上修饰符synchronized

在recover前,直接加上synchronized ,其所对应的同步对象,就是this和hurt方法达到的效果是一样。

外部线程访问gareen的方法,就不需要额外使用synchronized 了。

Hero.java

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
package charactor;

public class Hero{
public String name;
public float hp;

public int damage;

//回血
//直接在方法前加上修饰符synchronized
//其所对应的同步对象,就是this
//和hurt方法达到的效果一样
public synchronized void recover(){
hp=hp+1;
}

//掉血
public void hurt(){
//使用this作为同步对象
synchronized (this) {
hp=hp-1;
}
}

public void attackHero(Hero h) {
h.hp-=damage;
System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n",name,h.name,h.name,h.hp);
if(h.isDead())
System.out.println(h.name +"死了!");
}

public boolean isDead() {
return 0>=hp?true:false;
}

}

TestThread.java

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
67
68
69
70
71
72
73
74
75
76
77
78
79
package multiplethread;

import java.awt.GradientPaint;

import charactor.Hero;

public class TestThread {

public static void main(String[] args) {

final Hero gareen = new Hero();
gareen.name = "盖伦";
gareen.hp = 10000;

int n = 10000;

Thread[] addThreads = new Thread[n];
Thread[] reduceThreads = new Thread[n];

for (int i = 0; i < n; i++) {
Thread t = new Thread(){
public void run(){

//recover自带synchronized
gareen.recover();

try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
t.start();
addThreads[i] = t;

}

for (int i = 0; i < n; i++) {
Thread t = new Thread(){
public void run(){
//hurt自带synchronized
gareen.hurt();

try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
t.start();
reduceThreads[i] = t;
}

for (Thread t : addThreads) {
try {
t.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
for (Thread t : reduceThreads) {
try {
t.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

System.out.printf("%d个增加线程和%d个减少线程结束后%n盖伦的血量是 %.0f%n", n,n,gareen.hp);

}

}

线程安全的类

如果一个类,其方法都是有synchronized修饰的,那么该类就叫做线程安全的类

同一时间,只有一个线程能够进入 这种类的一个实例 的去修改数据,进而保证了这个实例中的数据的安全(不会同时被多线程修改而变成脏数据)

比如StringBufferStringBuilder的区别

  • StringBuffer的方法都是有synchronized修饰的,StringBuffer就叫做线程安全的类;
  • 而StringBuilder就不是线程安全的类。

常见的线程安全相关的面试题

HashMap和Hashtable的区别

相同点:

  • HashMap和Hashtable都实现了Map接口,都是键值对保存数据的方式。

区别1:

  • HashMap可以存放 null
  • Hashtable不能存放null

区别2:

  • HashMap不是线程安全的类
  • Hashtable是线程安全的类

StringBuffer 和 StringBuilder 的区别

StringBuffer 是线程安全的。
StringBuilder 是非线程安全的。

所以当进行大量字符串拼接操作的时候,如果是单线程就用StringBuilder会更快些,如果是多线程,就需要用StringBuffer 保证数据的安全性。

非线程安全的为什么会比线程安全的快? 因为不需要同步嘛,省略了些时间。

ArrayList 和 Vector 的区别

Vector是线程安全的
ArrayList是非线程安全的

ArrayList类的声明:

1
2
3
4
5
6
7
8
9
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
```


> Vector类的声明:
```java
public class Vector<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable

把非线程安全的集合转换为线程安全

ArrayList是非线程安全的,换句话说,多个线程可以同时进入一个ArrayList对象的add方法。

借助Collections.synchronizedList,可以把ArrayList转换为线程安全的List。

与此类似的,还有HashSet, LinkedList, HashMap等等非线程安全的类,都通过工具类Collections转换为线程安全的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package multiplethread;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class TestThread {

public static void main(String[] args) {
List<Integer> list1 = new ArrayList<>();
List<Integer> list2 = Collections.synchronizedList(list1);
}

}

多线程死锁

演示死锁

  1. 线程1 首先占有对象1,接着试图占有对象2
  2. 线程2 首先占有对象2,接着试图占有对象1
  3. 线程1 等待线程2释放对象2
  4. 与此同时,线程2等待线程1释放对象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
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
package multiplethread;

import charactor.Hero;

public class TestThread {

public static void main(String[] args) {
final Hero ahri = new Hero();
ahri.name = "九尾妖狐";
final Hero annie = new Hero();
annie.name = "安妮";

Thread t1 = new Thread(){
public void run(){
//占有九尾妖狐
synchronized (ahri) {
System.out.println("t1 已占有九尾妖狐");
try {
//停顿1000毫秒,另一个线程有足够的时间占有安妮
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

System.out.println("t1 试图占有安妮");
System.out.println("t1 等待中 。。。。");
synchronized (annie) {
System.out.println("do something");
}
}

}
};
t1.start();
Thread t2 = new Thread(){
public void run(){
//占有安妮
synchronized (annie) {
System.out.println("t2 已占有安妮");
try {

//停顿1000毫秒,另一个线程有足够的时间占有暂用九尾妖狐
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("t2 试图占有九尾妖狐");
System.out.println("t2 等待中 。。。。");
synchronized (ahri) {
System.out.println("do something");
}
}

}
};
t2.start();
}

}

线程之间的交互 - WAIT 和 NOTIFY

线程之间有交互通知的需求,考虑如下情况:
有两个线程,处理同一个英雄。 一个加血,一个减血。

减血的线程,发现血量=1,就停止减血,直到加血的线程为英雄加了血,才可以继续减血。

不好的解决方式

故意设计减血线程频率更高,盖伦的血量迟早会到达1。减血线程中使用while循环判断是否是1,如果是1就不停的循环,直到加血线程回复了血量。

这是不好的解决方式,因为会大量占用CPU,拖慢性能。

Hero.java

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
package charactor;

public class Hero{
public String name;
public float hp;

public int damage;

public synchronized void recover(){
hp=hp+1;
}

public synchronized void hurt(){
hp=hp-1;
}

public void attackHero(Hero h) {
h.hp-=damage;
System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n",name,h.name,h.name,h.hp);
if(h.isDead())
System.out.println(h.name +"死了!");
}

public boolean isDead() {
return 0>=hp?true:false;
}

}

TestThread.java

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
package multiplethread;

import java.awt.GradientPaint;

import charactor.Hero;

public class TestThread {

public static void main(String[] args) {

final Hero gareen = new Hero();
gareen.name = "盖伦";
gareen.hp = 616;

Thread t1 = new Thread(){
public void run(){
while(true){

//因为减血更快,所以盖伦的血量迟早会到达1
//使用while循环判断是否是1,如果是1就不停的循环
//直到加血线程回复了血量
while(gareen.hp==1){
continue;
}

gareen.hurt();
System.out.printf("t1 为%s 减血1点,减少血后,%s的血量是%.0f%n",gareen.name,gareen.name,gareen.hp);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

}
};
t1.start();

Thread t2 = new Thread(){
public void run(){
while(true){
gareen.recover();
System.out.printf("t2 为%s 回血1点,增加血后,%s的血量是%.0f%n",gareen.name,gareen.name,gareen.hp);

try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

}
};
t2.start();

}

}

使用wait和notify进行线程交互

在Hero类中:
hurt()减血方法:当hp=1的时候,执行this.wait();
this.wait()表示让占有this的线程等待,并临时释放占有。进入hurt方法的线程必然是减血线程,this.wait()会让减血线程临时释放对this的占有。 这样加血线程,就有机会进入recover()加血方法了。

recover() 加血方法:增加了血量,执行this.notify();
this.notify() 表示通知那些等待在this的线程,可以苏醒过来了。 等待在this的线程,恰恰就是减血线程。 一旦recover()结束, 加血线程释放了this,减血线程,就可以重新占有this,并执行后面的减血工作。

Hero.java

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
package charactor;

public class Hero {
public String name;
public float hp;

public int damage;

public synchronized void recover() {
hp = hp + 1;
System.out.printf("%s 回血1点,增加血后,%s的血量是%.0f%n", name, name, hp);
// 通知那些等待在this对象上的线程,可以醒过来了,如第20行,等待着的减血线程,苏醒过来
this.notify();
}

public synchronized void hurt() {
if (hp == 1) {
try {
// 让占有this的减血线程,暂时释放对this的占有,并等待
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

hp = hp - 1;
System.out.printf("%s 减血1点,减少血后,%s的血量是%.0f%n", name, name, hp);
}

public void attackHero(Hero h) {
h.hp -= damage;
System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n", name, h.name, h.name, h.hp);
if (h.isDead())
System.out.println(h.name + "死了!");
}

public boolean isDead() {
return 0 >= hp ? true : false;
}

}

TestThread.java

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
package multiplethread;

import java.awt.GradientPaint;

import charactor.Hero;

public class TestThread {

public static void main(String[] args) {

final Hero gareen = new Hero();
gareen.name = "盖伦";
gareen.hp = 616;

Thread t1 = new Thread(){
public void run(){
while(true){

//无需循环判断
// while(gareen.hp==1){
// continue;
// }

gareen.hurt();

try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

}
};
t1.start();

Thread t2 = new Thread(){
public void run(){
while(true){
gareen.recover();

try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

}
};
t2.start();

}

}

关于wait、notify 和 notifyAll

留意wait()和notify() 这两个方法是什么对象上的?

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
public synchronized void hurt() {
...
this.wait();
...
}
```

```
public synchronized void recover() {
...
this.notify();
}
```


> 这里需要强调的是,wait方法和notify方法,`并不是Thread线程上的方法`,它们是Object上的方法。 因为所有的Object都可以被用来作为同步对象,所以准确的讲,wait和notify是同步对象上的方法。

> `wait()`的意思是: 让占用了这个同步对象的线程,临时释放当前的占用,并且等待。 所以调用wait是有前提条件的,一定是在synchronized块里,否则就会出错。

> `notify()`的意思是,通知一个等待在这个同步对象上的线程,你可以苏醒过来了,有机会重新占用当前对象了。

> `notifyAll()` 的意思是,通知所有的等待在这个同步对象上的线程,你们可以苏醒过来了,有机会重新占用当前对象了。


## 线程池
> 每一个线程的启动和结束都是比较消耗时间和占用资源的。 如果在系统中用到了很多的线程,大量的启动和结束动作会导致系统的性能变卡,响应变慢。

> 为了解决这个问题,引入`线程池`这种设计思想。 线程池的模式很像生产者消费者模式,消费的对象是一个一个的能够运行的`任务`。

### 线程池设计思路
> 线程池的思路和生产者消费者模型是很接近的。
1. 准备一个任务容器
2. 一次性启动10个 消费者线程
3. 刚开始任务容器是空的,所以线程都wait在上面。
4. 直到一个外部线程往这个任务容器中扔了一个“任务”,就会有一个消费者线程被唤醒notify
5. 这个消费者线程取出“任务”,并且执行这个任务,执行完毕后,继续等待下一次任务的到来。
6. 如果短时间内,有较多的任务加入,那么就会有多个线程被唤醒,去执行这些任务。

> 在整个过程中,都不需要创建新的线程,而是循环使用这些已经存在的线程。

![](http://stepimagewm.how2j.cn/2600.png)

### 开发一个自定义线程池
> 这是一个自定义的线程池,虽然不够完善和健壮,但是已经足以说明线程池的工作原理

> 缓慢的给这个线程池添加任务,会看到有多条线程来执行这些任务。
线程7执行完毕任务后,`又回到池子里`,下一次任务来的时候,线程7又来执行新的任务。

![](http://stepimagewm.how2j.cn/1006.png)

`ThreadPool.java`
```java
package multiplethread;

import java.util.LinkedList;

public class ThreadPool {

// 线程池大小
int threadPoolSize;

// 任务容器
LinkedList<Runnable> tasks = new LinkedList<Runnable>();

// 试图消费任务的线程

public ThreadPool() {
threadPoolSize = 10;

// 启动10个任务消费者线程
synchronized (tasks) {
for (int i = 0; i < threadPoolSize; i++) {
new TaskConsumeThread("任务消费者线程 " + i).start();
}
}
}

public void add(Runnable r) {
synchronized (tasks) {
tasks.add(r);
// 唤醒等待的任务消费者线程
tasks.notifyAll();
}
}

class TaskConsumeThread extends Thread {
public TaskConsumeThread(String name) {
super(name);
}

Runnable task;

public void run() {
System.out.println("启动: " + this.getName());
while (true) {
synchronized (tasks) {
while (tasks.isEmpty()) {
try {
tasks.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
task = tasks.removeLast();
// 允许添加任务的线程可以继续添加任务
tasks.notifyAll();

}
System.out.println(this.getName() + " 获取到任务,并执行");
task.run();
}
}
}

}

TestThread.java

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
package multiplethread;

public class TestThread {

public static void main(String[] args) {
ThreadPool pool = new ThreadPool();

for (int i = 0; i < 5; i++) {
Runnable task = new Runnable() {
@Override
public void run() {
//System.out.println("执行任务");
//任务可能是打印一句话
//可能是访问文件
//可能是做排序
}
};

pool.add(task);

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

}

}

测试线程池

创造一个情景,每个任务执行的时间都是1秒。刚开始是间隔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
28
29
30
31
package multiplethread;

public class TestThread {
public static void main(String[] args) {
ThreadPool pool= new ThreadPool();
int sleep=1000;
while(true){
pool.add(new Runnable(){
@Override
public void run() {
//System.out.println("执行任务");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
try {
Thread.sleep(sleep);
sleep = sleep>100?sleep-100:sleep;
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}

}
}

使用java自带线程池

java提供自带的线程池,而不需要自己去开发一个自定义线程池了。

线程池类ThreadPoolExecutor在包java.util.concurrent

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
ThreadPoolExecutor threadPool= new ThreadPoolExecutor(10, 15, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
```

>
- 第一个参数10 表示这个线程池`初始化了10个线程`在里面工作;
- 第二个参数15 表示如果10个线程不够用了,就会自动增加到`最多15个线程`;
- 第三个参数60 结合第四个参数 TimeUnit.SECONDS,表示经过60秒,多出来的线程还没有接- 到活儿,就会回收,最后保持池子里就10个;
- 第四个参数 TimeUnit.SECONDS 如上;
- 第五个参数 new LinkedBlockingQueue() 用来放任务的集合;

> `execute`方法用于添加新的任务。

```java
package multiplethread;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class TestThread {

public static void main(String[] args) throws InterruptedException {

ThreadPoolExecutor threadPool= new ThreadPoolExecutor(10, 15, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());

threadPool.execute(new Runnable(){

@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("任务1");
}

});

}

}

Lock对象

与synchronized类似的,lock也能够达到同步的效果。

回忆 synchronized 同步的方式

首先回忆一下 synchronized 同步对象的方式。当一个线程占用 synchronized 同步对象,其他线程就不能占用了,直到释放这个同步对象为止。

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
package multiplethread;

import java.text.SimpleDateFormat;
import java.util.Date;

public class TestThread {

public static String now(){
return new SimpleDateFormat("HH:mm:ss").format(new Date());
}

public static void main(String[] args) {
final Object someObject = new Object();

Thread t1 = new Thread(){
public void run(){
try {
System.out.println( now()+" t1 线程已经运行");
System.out.println( now()+this.getName()+ " 试图占有对象:someObject");
synchronized (someObject) {

System.out.println( now()+this.getName()+ " 占有对象:someObject");
Thread.sleep(5000);
System.out.println( now()+this.getName()+ " 释放对象:someObject");
}
System.out.println(now()+" t1 线程结束");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
t1.setName(" t1");
t1.start();
Thread t2 = new Thread(){

public void run(){
try {
System.out.println( now()+" t2 线程已经运行");
System.out.println( now()+this.getName()+ " 试图占有对象:someObject");
synchronized (someObject) {
System.out.println( now()+this.getName()+ " 占有对象:someObject");
Thread.sleep(5000);
System.out.println( now()+this.getName()+ " 释放对象:someObject");
}
System.out.println(now()+" t2 线程结束");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
t2.setName(" t2");
t2.start();
}

}

使用Lock对象实现同步效果

Lock是一个接口,为了使用一个Lock对象,需要用到

1
Lock lock = new ReentrantLock();

synchronized (someObject) 类似的,lock()方法,表示当前线程占用lock对象,一旦占用,其他线程就不能占用了。
synchronized 不同的是,一旦synchronized 块结束,就会自动释放对someObject的占用。 lock却必须调用unlock方法进行手动释放,为了保证释放的执行,往往会把unlock() 放在finally中进行。

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
67
68
69
70
71
72
73
74
75
76
77
package multiplethread;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TestThread {

public static String now() {
return new SimpleDateFormat("HH:mm:ss").format(new Date());
}

public static void log(String msg) {
System.out.printf("%s %s %s %n", now() , Thread.currentThread().getName() , msg);
}

public static void main(String[] args) {
Lock lock = new ReentrantLock();

Thread t1 = new Thread() {
public void run() {
try {
log("线程启动");
log("试图占有对象:lock");

lock.lock();

log("占有对象:lock");
log("进行5秒的业务操作");
Thread.sleep(5000);

} catch (InterruptedException e) {
e.printStackTrace();
} finally {
log("释放对象:lock");
lock.unlock();
}
log("线程结束");
}
};
t1.setName("t1");
t1.start();
try {
//先让t1飞2秒
Thread.sleep(2000);
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
Thread t2 = new Thread() {

public void run() {
try {
log("线程启动");
log("试图占有对象:lock");

lock.lock();

log("占有对象:lock");
log("进行5秒的业务操作");
Thread.sleep(5000);

} catch (InterruptedException e) {
e.printStackTrace();
} finally {
log("释放对象:lock");
lock.unlock();
}
log("线程结束");
}
};
t2.setName("t2");
t2.start();
}

}

trylock方法

synchronized不占用到手不罢休的,会一直试图占用下去。
与 synchronized 的钻牛角尖不一样,Lock接口还提供了一个trylock方法。

trylock会在指定时间范围内试图占用,占成功了,就啪啪啪。 如果时间到了,还占用不成功,扭头就走~

注意: 因为使用trylock有可能成功,有可能失败,所以后面unlock释放锁的时候,需要判断是否占用成功了,如果没占用成功也unlock,就会抛出异常。

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
package multiplethread;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TestThread {

public static String now() {
return new SimpleDateFormat("HH:mm:ss").format(new Date());
}

public static void log(String msg) {
System.out.printf("%s %s %s %n", now() , Thread.currentThread().getName() , msg);
}

public static void main(String[] args) {
Lock lock = new ReentrantLock();

Thread t1 = new Thread() {
public void run() {
boolean locked = false;
try {
log("线程启动");
log("试图占有对象:lock");

locked = lock.tryLock(1,TimeUnit.SECONDS);
if(locked){
log("占有对象:lock");
log("进行5秒的业务操作");
Thread.sleep(5000);
}
else{
log("经过1秒钟的努力,还没有占有对象,放弃占有");
}

} catch (InterruptedException e) {
e.printStackTrace();
} finally {

if(locked){
log("释放对象:lock");
lock.unlock();
}
}
log("线程结束");
}
};
t1.setName("t1");
t1.start();
try {
//先让t1飞2秒
Thread.sleep(2000);
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
Thread t2 = new Thread() {

public void run() {
boolean locked = false;
try {
log("线程启动");
log("试图占有对象:lock");

locked = lock.tryLock(1,TimeUnit.SECONDS);
if(locked){
log("占有对象:lock");
log("进行5秒的业务操作");
Thread.sleep(5000);
}
else{
log("经过1秒钟的努力,还没有占有对象,放弃占有");
}

} catch (InterruptedException e) {
e.printStackTrace();
} finally {

if(locked){
log("释放对象:lock");
lock.unlock();
}
}
log("线程结束");
}
};
t2.setName("t2");
t2.start();
}

}

线程交互

使用synchronized方式进行线程交互,用到的是同步对象的wait,notify和notifyAll方法

Lock也提供了类似的解决办法,首先通过lock对象得到一个Condition对象,然后分别调用这个Condition对象的:await, signal, signalAll 方法

注意: 不是 Condition 对象的 wait, nofity, notifyAll 方法,而是 await, signal, signalAll 方法。

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
package multiplethread;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TestThread {

public static String now() {
return new SimpleDateFormat("HH:mm:ss").format(new Date());
}

public static void log(String msg) {
System.out.printf("%s %s %s %n", now() , Thread.currentThread().getName() , msg);
}

public static void main(String[] args) {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();

Thread t1 = new Thread() {
public void run() {
try {
log("线程启动");
log("试图占有对象:lock");

lock.lock();

log("占有对象:lock");
log("进行5秒的业务操作");
Thread.sleep(5000);
log("临时释放对象 lock, 并等待");
condition.await();
log("重新占有对象 lock,并进行5秒的业务操作");
Thread.sleep(5000);

} catch (InterruptedException e) {
e.printStackTrace();
} finally {
log("释放对象:lock");
lock.unlock();
}
log("线程结束");
}
};
t1.setName("t1");
t1.start();
try {
//先让t1飞2秒
Thread.sleep(2000);
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
Thread t2 = new Thread() {

public void run() {
try {
log("线程启动");
log("试图占有对象:lock");

lock.lock();

log("占有对象:lock");
log("进行5秒的业务操作");
Thread.sleep(5000);
log("唤醒等待中的线程");
condition.signal();

} catch (InterruptedException e) {
e.printStackTrace();
} finally {
log("释放对象:lock");
lock.unlock();
}
log("线程结束");
}
};
t2.setName("t2");
t2.start();
}

}

总结 Lock 和 synchronized 的区别

  1. Lock是一个接口,是代码层面的实现;
    synchronized是Java中的关键字,是内置的语言实现;
  1. Lock可以选择性的获取锁,如果一段时间获取不到,可以放弃。借助Lock的这个特性,就能够规避死锁;
    synchronized不行,会一根筋一直获取下去。 synchronized必须通过谨慎和良好的设计,才能减少死锁的发生;
  1. Lock必须手动释放, 所以如果忘记了释放锁,一样会造成死锁;
    synchronized在发生异常和同步块结束的时候,会自动释放锁;

原子访问

原子性操作概念

所谓的原子性操作即不可中断的操作,比如赋值操作:

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
int i = 5;
```

> 原子性操作本身是线程安全的,但是 i++ 这个行为,事实上是有3个原子性操作组成的。
1. 取 i 的值;
2. i + 1;
3. 把新的值赋予i;

> 这三个步骤,每一步都是一个原子操作,但是合在一起,就不是原子操作,`就不是线程安全的`。 换句话说,一个线程在步骤1 取i 的值结束后,还没有来得及进行步骤2,另一个线程也可以取 i的值了。

> 这也是分析同步问题产生的原因 中的原理。`i++ ,i--, i = i+1` 这些都是`非原子性操作`。
只有`int i = 1`,这个赋值操作是`原子性`的。

### AtomicInteger
> JDK6 以后,新增加了一个包`java.util.concurrent.atomic`,里面有各种原子类,比如`AtomicInteger`。

> 而AtomicInteger提供了各种自增,自减等方法,这些方法都是原子性的。 换句话说,自增方法 `incrementAndGet` 是线程安全的,同一个时间,只有一个线程可以调用这个方法。

```java
package multiplethread;

import java.util.concurrent.atomic.AtomicInteger;

public class TestThread {

public static void main(String[] args) throws InterruptedException {
AtomicInteger atomicI =new AtomicInteger();
int i = atomicI.decrementAndGet();
int j = atomicI.incrementAndGet();
int k = atomicI.addAndGet(3);

}

}

同步测试

分别使用基本变量的非原子性的++运算符原子性的AtomicInteger对象incrementAndGet 来进行多线程测试。

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
package multiplethread;

import java.util.concurrent.atomic.AtomicInteger;

public class TestThread {

private static int value = 0;
private static AtomicInteger atomicValue =new AtomicInteger();
public static void main(String[] args) {
int number = 100000;
Thread[] ts1 = new Thread[number];
for (int i = 0; i < number; i++) {
Thread t =new Thread(){
public void run(){
value++;
}
};
t.start();
ts1[i] = t;
}

//等待这些线程全部结束
for (Thread t : ts1) {
try {
t.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

System.out.printf("%d个线程进行value++后,value的值变成:%d%n", number,value);
Thread[] ts2 = new Thread[number];
for (int i = 0; i < number; i++) {
Thread t =new Thread(){
public void run(){
atomicValue.incrementAndGet();
}
};
t.start();
ts2[i] = t;
}

//等待这些线程全部结束
for (Thread t : ts2) {
try {
t.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.printf("%d个线程进行atomicValue.incrementAndGet();后,atomicValue的值变成:%d%n", number,atomicValue.intValue());
}

}

JDBC

准备工作

为项目导入mysql-jdbc的jar包

访问MySQL数据库需要用到第三方的类,这些第三方的类,都被压缩在一个叫做Jar的文件里。为了代码能够使用第三方的类,需要为项目导入mysql的专用Jar包mysql-connector-java-5.0.8-bin.jar
通常都会把项目用到的jar包统一放在项目的lib目录下,在本例就会放在
E:\project\j2se\lib 这个位置,然后在eclipse中导入这个jar包。

初始化驱动

通过Class.forName(“com.mysql.jdbc.Driver”);
初始化驱动类com.mysql.jdbc.Driver
就在 mysql-connector-java-5.0.8-bin.jar中
如果忘记了第一个步骤的导包,就会抛出ClassNotFoundException

Class.forName是把这个类加载到JVM中,加载的时候,就会执行其中的静态初始化块,完成驱动的初始化的相关工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package jdbc;

public class TestJDBC {
public static void main(String[] args) {

//初始化驱动
try {
//驱动类com.mysql.jdbc.Driver
//就在 mysql-connector-java-5.0.8-bin.jar中
//如果忘记了第一个步骤的导包,就会抛出ClassNotFoundException
Class.forName("com.mysql.jdbc.Driver");

System.out.println("数据库驱动加载成功 !");

} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}

建立与数据库的连接

建立与数据库的Connection连接
这里需要提供:
数据库所处于的ip:127.0.0.1 (本机)
数据库的端口号: 3306 (mysql专用端口号)
数据库名称 how2java
编码方式 UTF-8
账号 root
密码 admin

注: 这一步要成功执行,必须建立在mysql中有数据库how2java的基础上,如果没有,点击创建数据库查看如何进行数据库的创建。

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
package jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class TestJDBC {
public static void main(String[] args) {

try {
Class.forName("com.mysql.jdbc.Driver");

// 建立与数据库的Connection连接
// 这里需要提供:
// 数据库所处于的ip:127.0.0.1 (本机)
// 数据库的端口号: 3306 (mysql专用端口号)
// 数据库名称 how2java
// 编码方式 UTF-8
// 账号 root
// 密码 admin

Connection c = DriverManager
.getConnection(
"jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8",
"root", "admin");

System.out.println("连接成功,获取连接对象: " + c);

} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}

创建Statement

Statement是用于执行SQL语句的,比如增加,删除。

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
package jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class TestJDBC {
public static void main(String[] args) {

try {
Class.forName("com.mysql.jdbc.Driver");

Connection c = DriverManager
.getConnection(
"jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8",
"root", "admin");

// 注意:使用的是 java.sql.Statement
// 不要不小心使用到: com.mysql.jdbc.Statement;
Statement s = c.createStatement();

System.out.println("获取 Statement对象: " + s);

} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}

执行SQL语句

s.execute执行sql语句
执行成功后,用mysql-front进行查看,明确插入成功。

执行SQL语句之前要确保数据库how2java中有表hero的存在,如果没有,需要事先创建表。

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
package jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class TestJDBC {
public static void main(String[] args) {

try {
Class.forName("com.mysql.jdbc.Driver");

Connection c = DriverManager
.getConnection(
"jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8",
"root", "admin");

Statement s = c.createStatement();

// 准备sql语句
// 注意: 字符串要用单引号'
String sql = "insert into hero values(null,"+"'提莫'"+","+313.0f+","+50+")";
s.execute(sql);

System.out.println("执行插入语句成功");

} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}

关闭连接

数据库的连接是有限资源,相关操作结束后,养成关闭数据库的好习惯
先关闭Statement
后关闭Connection

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
package jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class TestJDBC {
public static void main(String[] args) {

Connection c = null;
Statement s = null;
try {
Class.forName("com.mysql.jdbc.Driver");

c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8", "root",
"admin");

s = c.createStatement();

String sql = "insert into hero values(null," + "'提莫'" + "," + 313.0f + "," + 50 + ")";

s.execute(sql);

} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
// 数据库的连接时有限资源,相关操作结束后,养成关闭数据库的好习惯
// 先关闭Statement
if (s != null)
try {
s.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 后关闭Connection
if (c != null)
try {
c.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}

}
}

使用 try-with-resource 的方式自动关闭连接

如果觉得上一步的关闭连接的方式很麻烦,可以参考关闭流 的方式,使用try-with-resource的方式自动关闭连接,因为Connection和Statement都实现了AutoCloseable接口。

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
package jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class TestJDBC {
public static void main(String[] args) {

try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

try (
Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8",
"root", "admin");
Statement s = c.createStatement();
)
{
String sql = "insert into hero values(null," + "'提莫'" + "," + 313.0f + "," + 50 + ")";
s.execute(sql);

} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

增、删、改

CRUD是最常见的数据库操作,即增删改查
C 增加(Create)
R 读取查询(Retrieve)
U 更新(Update)
D 删除(Delete)

在JDBC中增加,删除,修改的操作都很类似,只是传递不同的SQL语句就行了。

增加

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
package jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class TestJDBC {
public static void main(String[] args) {

try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

try (
Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8",
"root", "admin");
Statement s = c.createStatement();
)
{
String sql = "insert into hero values(null," + "'提莫'" + "," + 313.0f + "," + 50 + ")";
s.execute(sql);

} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

删除

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
package jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class TestJDBC {
public static void main(String[] args) {

try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

try (
Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8",
"root", "admin");
Statement s = c.createStatement();
)
{
String sql = "delete from hero where id = 5";
s.execute(sql);

} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

修改

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
package jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class TestJDBC {
public static void main(String[] args) {

try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

try (
Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8",
"root", "admin");
Statement s = c.createStatement();
)
{
String sql = "update hero set name = 'name 5' where id = 3";
s.execute(sql);

} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

查询

查询语句

executeQuery 执行SQL查询语句

注意: 在取第二列的数据的时候,用的是rs.get(2) ,而不是get(1). 这个是整个Java自带的api里唯二的地方,使用基1的,即2就代表第二个。

另一个地方是在PreparedStatement这里。

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
package jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class TestJDBC {
public static void main(String[] args) {
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

try (Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8",
"root", "admin"); Statement s = c.createStatement();) {

String sql = "select * from hero";

// 执行查询语句,并把结果集返回给ResultSet
ResultSet rs = s.executeQuery(sql);
while (rs.next()) {
int id = rs.getInt("id");// 可以使用字段名
String name = rs.getString(2);// 也可以使用字段的顺序
float hp = rs.getFloat("hp");
int damage = rs.getInt(4);
System.out.printf("%d\t%s\t%f\t%d%n", id, name, hp, damage);
}
// 不一定要在这里关闭ReultSet,因为Statement关闭的时候,会自动关闭ResultSet
// rs.close();

} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

SQL语句判断账号密码是否正确

1.创建一个用户表,有字段name,password
2.插入一条数据

1
insert into user values(null,'dashen','thisispassword');

3.SQL语句判断账号密码是否正确

判断账号密码的正确方式是根据账号和密码到表中去找数据,如果有数据,就表明密码正确了,如果没数据,就表明密码错误。

不恰当的方式 是把uers表的数据全部查到内存中,挨个进行比较。 如果users表里有100万条数据呢? 内存都不够用的。

SQL

1
2
3
4
5
6
7
CREATE TABLE user (
id int(11) AUTO_INCREMENT,
name varchar(30) ,
password varchar(30),
PRIMARY KEY (id)
) ;
insert into user values(null,'dashen','thisispassword');

TestJDBC.java

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
package jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class TestJDBC {
public static void main(String[] args) {
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

try (Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8",
"root", "admin");
Statement s = c.createStatement();

) {
String name = "dashen";
//正确的密码是:thisispassword
String password = "thisispassword1";

String sql = "select * from user where name = '" + name +"' and password = '" + password+"'";

// 执行查询语句,并把结果集返回给ResultSet
ResultSet rs = s.executeQuery(sql);

if(rs.next())
System.out.println("账号密码正确");
else
System.out.println("账号密码错误");

} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}

获取总数

执行的sql语句为

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
select count(*) from hero
```

> 然后通过ResultSet获取出来。

![](http://stepimagewm.how2j.cn/1714.png)

```java
package jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class TestJDBC {
public static void main(String[] args) {
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

try (Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8",
"root", "admin"); Statement s = c.createStatement();) {

String sql = "select count(*) from hero";

ResultSet rs = s.executeQuery(sql);
int total = 0;
while (rs.next()) {
total = rs.getInt(1);
}

System.out.println("表Hero中总共有:" + total+" 条数据");

} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}

预编译Statement

使用PreparedStatement

和 Statement一样,PreparedStatement也是用来执行sql语句的
与创建Statement不同的是,需要根据sql语句创建PreparedStatement。
除此之外,还能够通过设置参数,指定相应的值,而不是Statement那样使用字符串拼接。

注: 这是JAVA里唯二基1的地方,另一个是查询语句中的ResultSet也是基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
28
29
30
31
32
33
34
35
package jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class TestJDBC {
public static void main(String[] args) {
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

String sql = "insert into hero values(null,?,?,?)";
try (Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8","root", "admin");
// 根据sql语句创建PreparedStatement
PreparedStatement ps = c.prepareStatement(sql);
) {

// 设置参数
ps.setString(1, "提莫");
ps.setFloat(2, 313.0f);
ps.setInt(3, 50);
// 执行
ps.execute();

} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}

PreparedStatement的优点1 - 参数设置

Statement 需要进行字符串拼接,可读性和维护性比较差

1
2
3
4
5
6
7
String sql = "insert into hero values(null,"+"'提莫'"+","+313.0f+","+50+")";
```

> `PreparedStatement` 使用参数设置,可读性好,不易犯错

```java
String sql = "insert into hero values(null,?,?,?)";

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
package jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;

public class TestJDBC {
public static void main(String[] args) {

try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

String sql = "insert into hero values(null,?,?,?)";
try (Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8","root", "admin");
Statement s = c.createStatement();
PreparedStatement ps = c.prepareStatement(sql);
) {
// Statement需要进行字符串拼接,可读性和维修性比较差
String sql0 = "insert into hero values(null," + "'提莫'" + "," + 313.0f + "," + 50 + ")";
s.execute(sql0);

// PreparedStatement 使用参数设置,可读性好,不易犯错
// "insert into hero values(null,?,?,?)";
ps.setString(1, "提莫");
ps.setFloat(2, 313.0f);
ps.setInt(3, 50);
ps.execute();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}

PreparedStatement的优点2 - 性能表现

PreparedStatement有预编译机制,性能比Statement更快。

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
package jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;

public class TestJDBC {
public static void main(String[] args) {

try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

String sql = "insert into hero values(null,?,?,?)";
try (Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8","root", "admin");
Statement s = c.createStatement();
PreparedStatement ps = c.prepareStatement(sql);
) {
// Statement执行10次,需要10次把SQL语句传输到数据库端
// 数据库要对每一次来的SQL语句进行编译处理
for (int i = 0; i < 10; i++) {
String sql0 = "insert into hero values(null," + "'提莫'" + ","
+ 313.0f + "," + 50 + ")";
s.execute(sql0);
}
s.close();

// PreparedStatement 执行10次,只需要1次把SQL语句传输到数据库端
// 数据库对带?的SQL进行预编译

// 每次执行,只需要传输参数到数据库端
// 1. 网络传输量比Statement更小
// 2. 数据库不需要再进行编译,响应更快
for (int i = 0; i < 10; i++) {
ps.setString(1, "提莫");
ps.setFloat(2, 313.0f);
ps.setInt(3, 50);
ps.execute();
}

} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}

PreparedStatement的优点3 - 防止SQL注入式攻击

假设name是用户提交来的数据

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
67
68
69
70
71
72
73
74
75
76
77
78
String name = "'盖伦' OR 1=1";
```

> 使用Statement就需要进行字符串拼接
拼接出来的语句是:

```sql
select * from hero where name = '盖伦' OR 1=1
```

> 因为有OR 1=1,这是恒成立的
那么就会把所有的英雄都查出来,而不只是盖伦。
如果Hero表里的数据是海量的,比如几百万条,`把这个表里的数据全部查出来`。
会让数据库负载变高,CPU100%,内存消耗光,`响应变得极其缓慢`。

> 而PreparedStatement使用的是`参数设置`,就不会有这个问题。

```java
package jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class TestJDBC {
public static void main(String[] args) {

try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

String sql = "select * from hero where name = ?";
try (Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8","root", "admin");
Statement s = c.createStatement();
PreparedStatement ps = c.prepareStatement(sql);
) {
// 假设name是用户提交来的数据
String name = "'盖伦' OR 1=1";
String sql0 = "select * from hero where name = " + name;
// 拼接出来的SQL语句就是
// select * from hero where name = '盖伦' OR 1=1
// 因为有OR 1=1,所以恒成立
// 那么就会把所有的英雄都查出来,而不只是盖伦
// 如果Hero表里的数据是海量的,比如几百万条,把这个表里的数据全部查出来
// 会让数据库负载变高,CPU100%,内存消耗光,响应变得极其缓慢
System.out.println(sql0);

ResultSet rs0 = s.executeQuery(sql0);
while (rs0.next()) {
String heroName = rs0.getString("name");
System.out.println(heroName);
}

s.execute(sql0);

// 使用预编译Statement就可以杜绝SQL注入

ps.setString(1, name);

ResultSet rs = ps.executeQuery();
// 查不出数据出来
while (rs.next()) {
String heroName = rs.getString("name");
System.out.println(heroName);
}

} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}

execute 与 executeUpdate 的区别

相同点

executeexecuteUpdate的相同点:都可以执行增加,删除,修改;

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
package jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class TestJDBC {
public static void main(String[] args) {
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

try (Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8","root", "admin");
Statement s = c.createStatement();) {

String sqlInsert = "insert into Hero values (null,'盖伦',616,100)";
String sqlDelete = "delete from Hero where id = 100";
String sqlUpdate = "update Hero set hp = 300 where id = 100";

// 相同点:都可以执行增加,删除,修改

s.execute(sqlInsert);
s.execute(sqlDelete);
s.execute(sqlUpdate);
s.executeUpdate(sqlInsert);
s.executeUpdate(sqlDelete);
s.executeUpdate(sqlUpdate);

} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}

不同点

不同1:

  • execute可以执行查询语句, 然后通过getResultSet,把结果集取出来;
  • executeUpdate不能执行查询语句;

不同2:

  • execute返回boolean类型,true表示执行的是查询语句,false表示执行的是insert, delete, update等等
  • executeUpdate返回的是int,表示有多少条数据受到了影响;
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
package jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class TestJDBC {
public static void main(String[] args) {

try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

try (Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8","root", "admin");
Statement s = c.createStatement();) {

// 不同1:execute可以执行查询语句
// 然后通过getResultSet,把结果集取出来
String sqlSelect = "select * from hero";

s.execute(sqlSelect);
ResultSet rs = s.getResultSet();
while (rs.next()) {
System.out.println(rs.getInt("id"));
}

// executeUpdate不能执行查询语句
// s.executeUpdate(sqlSelect);

// 不同2:
// execute返回boolean类型,true表示执行的是查询语句,false表示执行的是insert,delete,update等等
boolean isSelect = s.execute(sqlSelect);
System.out.println(isSelect);

// executeUpdate返回的是int,表示有多少条数据受到了影响
String sqlUpdate = "update Hero set hp = 300 where id < 100";
int number = s.executeUpdate(sqlUpdate);
System.out.println(number);

} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}

特殊操作

获取自增长id

在Statement通过execute或者executeUpdate执行完插入语句后,MySQL会为新插入的数据分配一个自增长id,(前提是这个表的id设置为了自增长,在Mysql创建表的时候,AUTO_INCREMENT就表示自增长)

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
CREATE TABLE hero (
id int(11) AUTO_INCREMENT,
...
}
```

> 但是无论是execute还是executeUpdate都不会返回这个自增长id是多少。需要通过`Statement`的`getGeneratedKeys`获取该id;

> 注: 第20行的代码,后面加了个`Statement.RETURN_GENERATED_KEYS`参数,以确保会返回自增长ID。 通常情况下不需要加这个,有的时候需要加,所以先加上,保险一些

```java
PreparedStatement ps = c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
```


```java
package jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class TestJDBC {

public static void main(String[] args) {
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
String sql = "insert into hero values(null,?,?,?)";
try (Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8","root", "admin");
PreparedStatement ps = c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
) {

ps.setString(1, "盖伦");
ps.setFloat(2, 616);
ps.setInt(3, 100);

// 执行插入语句
ps.execute();

// 在执行完插入语句后,MySQL会为新插入的数据分配一个自增长id
// JDBC通过getGeneratedKeys获取该id
ResultSet rs = ps.getGeneratedKeys();
if (rs.next()) {
int id = rs.getInt(1);
System.out.println(id);
}

} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}

获取表的元数据

元数据概念:
和数据库服务器相关的数据,比如数据库版本,有哪些表,表有哪些字段,字段类型是什么等等。

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
package jdbc;

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class TestJDBC {

public static void main(String[] args) throws Exception {
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

try (Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8","root", "admin");) {

// 查看数据库层面的元数据
// 即数据库服务器版本,驱动版本,都有哪些数据库等等

DatabaseMetaData dbmd = c.getMetaData();

// 获取数据库服务器产品名称
System.out.println("数据库产品名称:\t"+dbmd.getDatabaseProductName());
// 获取数据库服务器产品版本号
System.out.println("数据库产品版本:\t"+dbmd.getDatabaseProductVersion());
// 获取数据库服务器用作类别和表名之间的分隔符 如test.user
System.out.println("数据库和表分隔符:\t"+dbmd.getCatalogSeparator());
// 获取驱动版本
System.out.println("驱动版本:\t"+dbmd.getDriverVersion());

System.out.println("可用的数据库列表:");
// 获取数据库名称
ResultSet rs = dbmd.getCatalogs();

while (rs.next()) {
System.out.println("数据库名称:\t"+rs.getString(1));
}

} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}

事务

不使用事务的情况

没有事务的前提下
假设业务操作是:加血,减血各做一次。结束后,英雄的血量不变。
而减血的SQL,不小心写错写成了 updata(而非update),那么最后结果是血量增加了,而非期望的不变。

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
package jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class TestJDBC {
public static void main(String[] args) {
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

try (Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8","root", "admin");
Statement s = c.createStatement();) {

//没有事务的前提下
//假设业务操作时,加血,减血各做一次
//结束后,英雄的血量不变

//加血的SQL
String sql1 = "update hero set hp = hp +1 where id = 22";
s.execute(sql1);

//减血的SQL
//不小心写错写成了 updata(而非update)

String sql2 = "updata hero set hp = hp -1 where id = 22";
s.execute(sql2);

} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}

使用事务

在事务中的多个操作,要么都成功,要么都失败

  • 通过 c.setAutoCommit(false);关闭自动提交
  • 使用 c.commit(); 进行手动提交

在22行-35行之间的数据库操作,就处于同一个事务当中,要么都成功,要么都失败
所以,虽然第一条SQL语句是可以执行的,但是第二条SQL语句有错误,其结果就是两条SQL语句都没有被提交。 除非两条SQL语句都是正确的。

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
package jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class TestJDBC {
public static void main(String[] args) {
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

try (Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8","root", "admin");
Statement s = c.createStatement();) {

// 有事务的前提下
// 在事务中的多个操作,要么都成功,要么都失败

c.setAutoCommit(false);

// 加血的SQL
String sql1 = "update hero set hp = hp +1 where id = 22";
s.execute(sql1);

// 减血的SQL
// 不小心写错写成了 updata(而非update)

String sql2 = "updata hero set hp = hp -1 where id = 22";
s.execute(sql2);

// 手动提交
c.commit();

} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}

MYSQL 表的类型必须是INNODB才支持事务

在Mysql中,只有当表的类型是INNODB的时候,才支持事务,所以需要把表的类型设置为INNODB,否则无法观察到事务。
修改表的类型为INNODB的SQL:

alter table hero ENGINE = innodb;

查看表的类型的SQL

show table status from how2java;

不过有个前提,就是当前的MYSQL服务器本身要支持INNODB。

ORM

ORM = Object Relationship Database Mapping
对象和关系数据库的映射。简单说,一个对象,对应数据库里的一条记录。

根据id返回一个Hero对象

Hero.java

1
2
3
4
5
6
7
8
9
10
package charactor;

public class Hero {
//增加id属性
public int id;
public String name;
public float hp;
public int damage;

}

TestJDBC.java

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
package jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

import charactor.Hero;

public class TestJDBC {

public static Hero get(int id) {
Hero hero = null;
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

try (Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8","root", "admin");
Statement s = c.createStatement();) {

String sql = "select * from hero where id = " + id;

ResultSet rs = s.executeQuery(sql);

// 因为id是唯一的,ResultSet最多只能有一条记录
// 所以使用if代替while
if (rs.next()) {
hero = new Hero();
String name = rs.getString(2);
float hp = rs.getFloat("hp");
int damage = rs.getInt(4);
hero.name = name;
hero.hp = hp;
hero.damage = damage;
hero.id = id;
}

} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return hero;

}

public static void main(String[] args) {

Hero h = get(22);
System.out.println(h.name);

}
}

DAO

数据访问对象 DAO = DataAccess Object

DAO接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package jdbc;

import java.util.List;

import charactor.Hero;

public interface DAO{
//增加
public void add(Hero hero);
//修改
public void update(Hero hero);
//删除
public void delete(int id);
//获取
public Hero get(int id);
//查询
public List<Hero> list();
//分页查询
public List<Hero> list(int start, int count);
}

HeroDAO

设计类HeroDAO,实现接口DAO

这个HeroDAO和答案-ORM很接近,做了几个改进:
1.把驱动的初始化放在了构造方法HeroDAO里:

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
public HeroDAO() {
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
```

> 因为驱动初始化只需要执行一次,所以放在这里更合适,其他方法里也不需要写了,代码更简洁

> 2.提供了一个getConnection方法返回连接
所有的数据库操作都需要事先拿到一个数据库连接Connection,以前的做法每个方法里都会写一个,如果要改动密码,那么每个地方都需要修改。 通过这种方式,只需要修改这一个地方就可以了。 代码变得更容易维护,而且也更加简洁。



```java
package jdbc;

import java.sql.Connection;

import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

import charactor.Hero;

public class HeroDAO implements DAO{

public HeroDAO() {
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

public Connection getConnection() throws SQLException {
return DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8", "root",
"admin");
}

public int getTotal() {
int total = 0;
try (Connection c = getConnection(); Statement s = c.createStatement();) {

String sql = "select count(*) from hero";

ResultSet rs = s.executeQuery(sql);
while (rs.next()) {
total = rs.getInt(1);
}

System.out.println("total:" + total);

} catch (SQLException e) {

e.printStackTrace();
}
return total;
}

public void add(Hero hero) {

String sql = "insert into hero values(null,?,?,?)";
try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) {

ps.setString(1, hero.name);
ps.setFloat(2, hero.hp);
ps.setInt(3, hero.damage);

ps.execute();

ResultSet rs = ps.getGeneratedKeys();
if (rs.next()) {
int id = rs.getInt(1);
hero.id = id;
}
} catch (SQLException e) {

e.printStackTrace();
}
}

public void update(Hero hero) {

String sql = "update hero set name= ?, hp = ? , damage = ? where id = ?";
try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) {

ps.setString(1, hero.name);
ps.setFloat(2, hero.hp);
ps.setInt(3, hero.damage);
ps.setInt(4, hero.id);

ps.execute();

} catch (SQLException e) {

e.printStackTrace();
}

}

public void delete(int id) {

try (Connection c = getConnection(); Statement s = c.createStatement();) {

String sql = "delete from hero where id = " + id;

s.execute(sql);

} catch (SQLException e) {

e.printStackTrace();
}
}

public Hero get(int id) {
Hero hero = null;

try (Connection c = getConnection(); Statement s = c.createStatement();) {

String sql = "select * from hero where id = " + id;

ResultSet rs = s.executeQuery(sql);

if (rs.next()) {
hero = new Hero();
String name = rs.getString(2);
float hp = rs.getFloat("hp");
int damage = rs.getInt(4);
hero.name = name;
hero.hp = hp;
hero.damage = damage;
hero.id = id;
}

} catch (SQLException e) {

e.printStackTrace();
}
return hero;
}

public List<Hero> list() {
return list(0, Short.MAX_VALUE);
}

public List<Hero> list(int start, int count) {
List<Hero> heros = new ArrayList<Hero>();

String sql = "select * from hero order by id desc limit ?,? ";

try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) {

ps.setInt(1, start);
ps.setInt(2, count);

ResultSet rs = ps.executeQuery();

while (rs.next()) {
Hero hero = new Hero();
int id = rs.getInt(1);
String name = rs.getString(2);
float hp = rs.getFloat("hp");
int damage = rs.getInt(4);
hero.id = id;
hero.name = name;
hero.hp = hp;
hero.damage = damage;
heros.add(hero);
}
} catch (SQLException e) {

e.printStackTrace();
}
return heros;
}

}

数据库连接池

数据库连接池原理 - 传统方式

当有多个线程,每个线程都需要连接数据库执行SQL语句的话,那么每个线程都会创建一个连接,并且在使用完毕后,关闭连接。

创建连接和关闭连接的过程也是比较消耗时间的,当多线程并发的时候,系统就会变得很卡顿。

同时,一个数据库同时支持的连接总数也是有限的,如果多线程并发量很大,那么数据库连接的总数就会被消耗光,后续线程发起的数据库连接就会失败。

数据库连接池原理 - 使用池

与传统方式不同,连接池在使用之前,就会创建好一定数量的连接。
如果有任何线程需要使用连接,那么就从连接池里面借用而不是自己重新创建
使用完毕后,又把这个连接归还给连接池供下一次或者其他线程使用。
倘若发生多线程并发情况,连接池里的连接被借用光了,那么其他线程就会临时等待,直到有连接被归还回来,再继续使用。
整个过程,这些连接都不会被关闭,而是不断的被循环使用,从而节约了启动和关闭连接的时间。

ConnectionPool构造方法和初始化

  1. ConnectionPool() 构造方法约定了这个连接池一共有多少连接;
  2. init() 初始化方法中,创建了size条连接。 注意,这里不能使用try-with-resource这种自动关闭连接的方式,因为连接恰恰需要保持不关闭状态,供后续循环使用;
  3. getConnection, 判断是否为空,如果是空的就wait等待,否则就借用一条连接出去;
  4. returnConnection, 在使用完毕后,归还这个连接到连接池,并且在归还完毕后,调用notifyAll,通知那些等待的线程,有新的连接可以借用了。

注:连接池设计用到了多线程的wait和notifyAll,这些内容可以在多线程交互章节查阅学习。

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
package jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

public class ConnectionPool {

List<Connection> cs = new ArrayList<Connection>();

int size;

public ConnectionPool(int size) {
this.size = size;
init();
}

public void init() {

//这里恰恰不能使用try-with-resource的方式,因为这些连接都需要是"活"的,不要被自动关闭了
try {
Class.forName("com.mysql.jdbc.Driver");
for (int i = 0; i < size; i++) {
Connection c = DriverManager
.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8", "root", "admin");

cs.add(c);

}
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

public synchronized Connection getConnection() {
while (cs.isEmpty()) {
try {
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
Connection c = cs.remove(0);
return c;
}

public synchronized void returnConnection(Connection c) {
cs.add(c);
this.notifyAll();
}

}

测试类

首先初始化一个有3条连接的数据库连接池
然后创建100个线程,每个线程都会从连接池中借用连接,并且在借用之后,归还连接。 拿到连接之后,执行一个耗时1秒的SQL语句。

运行程序,就可以观察到如图所示的效果:

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
package jdbc;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;

import jdbc.ConnectionPool;

public class TestConnectionPool {

public static void main(String[] args) {
ConnectionPool cp = new ConnectionPool(3);
for (int i = 0; i < 100; i++) {
new WorkingThread("working thread" + i, cp).start();
}

}
}

class WorkingThread extends Thread {
private ConnectionPool cp;

public WorkingThread(String name, ConnectionPool cp) {
super(name);
this.cp = cp;
}

public void run() {
Connection c = cp.getConnection();
System.out.println(this.getName()+ ":\t 获取了一根连接,并开始工作" );
try (Statement st = c.createStatement()){

//模拟时耗1秒的数据库SQL语句
Thread.sleep(1000);
st.execute("select * from hero");

} catch (SQLException | InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
cp.returnConnection(c);
}
}

反射机制

获取类对象

什么是类对象?

在理解类对象之前,先说我们熟悉的对象之间的区别:
garen和teemo都是Hero对象,他们的区别在于,各自有不同的名称,血量,伤害值。

然后说说类之间的区别:
Hero和Item都是类,他们的区别在于有不同的方法,不同的属性。

类对象,就是用于描述这种类,都有什么属性、什么方法的。

获取类对象

获取类对象有3种方式:

  1. Class.forName
  2. Hero.class
  3. new Hero().getClass()

在一个JVM中,一种类,只会有一个类对象存在。所以以上三种方式取出来的类对象,都是一样的。

注: 准确的讲是一个ClassLoader下,一种类,只会有一个类对象存在。通常一个JVM下,只会有一个ClassLoader。因为还没有引入ClassLoader概念, 所以暂时不展开了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package reflection;

import charactor.Hero;

public class TestReflection {

public static void main(String[] args) {
String className = "charactor.Hero";
try {
Class pClass1=Class.forName(className);
Class pClass2=Hero.class;
Class pClass3=new Hero().getClass();
System.out.println(pClass1==pClass2);
System.out.println(pClass1==pClass3);
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

获取类对象的时候,会导致类属性被初始化

为Hero增加一个静态属性,并且在静态初始化块里进行初始化,参考 类属性初始化。

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
static String copyright;
static {
System.out.println("初始化 copyright");
copyright = "版权由Riot Games公司所有";
}
```

> 无论什么途径获取类对象,都会导致静态属性被初始化,而且只会执行一次。(除了直接使用 `Class c = Hero.class` 这种方式,这种方式不会导致静态属性被初始化)

![](http://stepimagewm.how2j.cn/2708.png)

`Hero.java`
```java
package charactor;

public class Hero {
public String name;
public float hp;
public int damage;
public int id;

static String copyright;

static {
System.out.println("初始化 copyright");
copyright = "版权由Riot Games公司所有";
}

}

TestReflection.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package reflection;

import charactor.Hero;

public class TestReflection {

public static void main(String[] args) {
String className = "charactor.Hero";
try {
Class pClass1=Class.forName(className);
Class pClass2=Hero.class;
Class pClass3=new Hero().getClass();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

创建对象

与传统的通过new 来获取对象的方式不同。
反射机制,会先拿到Hero的“类对象”,然后通过类对象获取“构造器对象” ,再通过构造器对象创建一个对象

创建一个对象

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
package reflection;
import java.lang.reflect.Constructor;
import charactor.Hero;
public class TestReflection {

public static void main(String[] args) {
//传统的使用new的方式创建对象
Hero h1 =new Hero();
h1.name = "teemo";
System.out.println(h1);

try {
//使用反射的方式创建对象
String className = "charactor.Hero";
//类对象
Class pClass=Class.forName(className);
//构造器
Constructor c= pClass.getConstructor();
//通过构造器实例化
Hero h2= (Hero) c.newInstance();
h2.name="gareen";
System.out.println(h2);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

访问属性

通过反射机制修改对象的属性。

Hero.java

为了访问属性,把name修改为public。
对于private修饰的成员,需要使用setAccessible(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
package charactor;

public class Hero {
public String name;
public float hp;
public int damage;
public int id;

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

}
public Hero(String string) {
name =string;
}

@Override
public String toString() {
return "Hero [name=" + name + "]";
}
public boolean isDead() {
// TODO Auto-generated method stub
return false;
}
public void attackHero(Hero h2) {
System.out.println(this.name+ " 正在攻击 " + h2.getName());
}

}

TestRelection

通过反射修改属性的值。

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
package reflection;

import java.lang.reflect.Field;

import charactor.Hero;

public class TestReflection {

public static void main(String[] args) {
Hero h =new Hero();
//使用传统方式修改name的值为garen
h.name = "garen";
try {
//获取类Hero的名字叫做name的字段
Field f1= h.getClass().getDeclaredField("name");
//修改这个字段的值
f1.set(h, "teemo");
//打印被修改后的值
System.out.println(h.name);

} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

getField 和 getDeclaredField 的区别

getField和getDeclaredField的区别
这两个方法都是用于获取字段:

  • getField 只能获取public的,包括从父类继承来的字段。
  • getDeclaredField 可以获取本类所有的字段,包括private的,但是不能获取继承来的字段。

注: 这里只能获取到private的字段,但并不能访问该private字段的值,除非加上setAccessible(true)

调用方法

通过反射机制,调用一个对象的方法。

调用方法

首先为Hero的name属性,增加setter和getter,通过反射机制调用Hero的setName

Hero.java

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
package charactor;

public class Hero {
public String name;
public float hp;
public int damage;
public int id;

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

}
public Hero(String string) {
name =string;
}

@Override
public String toString() {
return "Hero [name=" + name + "]";
}
public boolean isDead() {
// TODO Auto-generated method stub
return false;
}
public void attackHero(Hero h2) {
// TODO Auto-generated method stub

}

}

TestReflection.java

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
package reflection;

import java.lang.reflect.Method;

import charactor.Hero;

public class TestReflection {

public static void main(String[] args) {
Hero h = new Hero();

try {
// 获取这个名字叫做setName,参数类型是String的方法
Method m = h.getClass().getMethod("setName", String.class);
// 对h对象,调用这个方法
m.invoke(h, "盖伦");
// 使用传统的方式,调用getName方法
System.out.println(h.getName());

} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}

反射机制的作用

反射非常强大,但是学习了之后,会不知道该如何使用,反而觉得还不如直接调用方法来的直接和方便。

通常来说,需要在学习了Spring 的依赖注入,反转控制之后,才会对反射有更好的理解,但是刚学到这里的同学,不一定接触了Spring,所以在这里举两个例子,来演示一下反射的一种实际运用。

业务类

首先准备两个业务类,这两个业务类很简单,就是各自都有一个业务方法,分别打印不同的字符串。

Service1.java

1
2
3
4
5
6
7
8
package reflection;

public class Service1 {

public void doService1(){
System.out.println("业务方法1");
}
}

Service2.java

1
2
3
4
5
6
7
8
package reflection;

public class Service2 {

public void doService2(){
System.out.println("业务方法2");
}
}

非反射方式

当需要从第一个业务方法切换到第二个业务方法的时候,使用非反射方式,必须修改代码,并且重新编译运行,才可以达到效果。

Test.java

1
2
3
4
5
6
7
8
package reflection;

public class Test {

public static void main(String[] args) {
new Service1().doService1();
}
}

Test.java

1
2
3
4
5
6
7
8
9
package reflection;

public class Test {

public static void main(String[] args) {
// new Service1().doService1();
new Service2().doService2();
}
}

反射方式

使用反射方式,首先准备一个配置文件,就叫做spring.txt吧, 放在src目录下。 里面存放的是类的名称,和要调用的方法名。
在测试类Test中,首先取出类名称和方法名,然后通过反射去调用这个方法。

当需要从调用第一个业务方法,切换到调用第二个业务方法的时候,不需要修改一行代码,也不需要重新编译,只需要修改配置文件spring.txt,再运行即可。

这也是Spring框架的最基本的原理,只是它做的更丰富,安全,健壮。

spring.txt

1
2
class=reflection.Service1
method=doService1

Test.java

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
package reflection;

import java.io.File;
import java.io.FileInputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Properties;

public class Test {

@SuppressWarnings({ "rawtypes", "unchecked" })
public static void main(String[] args) throws Exception {

//从spring.txt中获取类名称和方法名称
File springConfigFile = new File("e:\\project\\j2se\\src\\spring.txt");
Properties springConfig= new Properties();
springConfig.load(new FileInputStream(springConfigFile));
String className = (String) springConfig.get("class");
String methodName = (String) springConfig.get("method");

//根据类名称获取类对象
Class clazz = Class.forName(className);
//根据方法名称,获取方法对象
Method m = clazz.getMethod(methodName);
//获取构造器
Constructor c = clazz.getConstructor();
//根据构造器,实例化出对象
Object service = c.newInstance();
//调用对象的指定方法
m.invoke(service);

}
}

注解

注解简介

本模块首先简单介绍基本注解,然后借助自定义注解的方式,帮助大家理解 Hibernate,Spring, Struts等等第三方注解是如何工作的。
因为是高级内容,所以需要有如下前置基础:

  1. 反射:反射用于解析注解中的信息;
  2. 有任意使用注解方式使用框架的经验,如:Hibernate, Spring, Struts,Mybatis 等;

基本内置注解

@Override

@Override 用在方法上,表示这个方法重写了父类的方法,如toString()。
如果父类没有这个方法,那么就无法编译通过,如例所示,在fromString()方法上加上@Override 注解,就会失败,因为Hero类的父类Object,并没有fromString方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package annotation;

public class Hero {

String name;
@Override
public String toString(){
return name;
}
@Override
public String fromString(){
return name;
}
}

@Deprecated

@Deprecated 表示这个方法已经过期,不建议开发者使用。(暗示在将来某个不确定的版本,就有可能会取消掉)

如例所示,开地图这个方法hackMap,被注解为过期,在调用的时候,就会受到提示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package annotation;

public class Hero {

String name;

@Deprecated
public void hackMap(){

}
public static void main(String[] args) {
new Hero().hackMap();
}

}

@SuppressWarnings

@SuppressWarnings Suppress英文的意思是抑制的意思,这个注解的用处是忽略警告信息。
比如大家使用集合的时候,有时候为了偷懒,会不写泛型,像这样:

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
List heros = new ArrayList();
```

> 那么就会导致编译器出现警告,而加上

```java
@SuppressWarnings({ "rawtypes", "unused" })
```

> 就对这些警告进行了抑制,即忽略掉这些警告信息。
@SuppressWarnings 有常见的值,分别对应如下意思
> 1. `deprecation`:使用了不赞成使用的类或方法时的警告(使用@Deprecated使得编译器产生的警告);
> 2. `unchecked`:执行了未检查的转换时的警告,例如当使用集合时没有用泛型 (Generics) 来指定集合保存的类型; 关闭编译器警告
> 3. `fallthrough`:当 Switch 程序块直接通往下一种情况而没有 Break 时的警告;
> 4. `path`:在类路径、源文件路径等中有不存在的路径时的警告;
> 5. `serial`:当在可序列化的类上缺少 serialVersionUID 定义时的警告;
> 6. `finally`:任何 finally 子句不能正常完成时的警告;
> 7. `rawtypes`:泛型类型未指明
> 8. `unused`: 引用定义了,但是没有被使用
> 9. `all`:关于以上所有情况的警告。

```java
package annotation;

import java.util.ArrayList;
import java.util.List;

public class Hero {
String name;
@SuppressWarnings({ "rawtypes", "unused" })
public static void main(String[] args) {
List heros = new ArrayList();
}

}

@SafeVarargs

@SafeVarargs 这是1.7 之后新加入的基本注解. 如例所示,当使用可变数量的参数的时候,而参数的类型又是泛型T的话,就会出现警告。 这个时候,就使用@SafeVarargs来去掉这个警告

@SafeVarargs注解只能用在参数长度可变的方法或构造方法上,且方法必须声明为static或final,否则会出现编译错误。一个方法使用@SafeVarargs注解的前提是,开发人员必须确保这个方法的实现中对泛型类型参数的处理不会引发类型安全问题。

1
2
3
4
5
6
7
8
9
10
11

package annotation;

public class Hero {
String name;

@SafeVarargs
public static <T> T getFirstOne(T... elements) {
return elements.length > 0 ? elements[0] : null;
}
}

@FunctionalInterface

@FunctionalInterface这是Java1.8 新增的注解,用于约定函数式接口。
函数式接口概念: 如果接口中只有一个抽象方法(可以包含多个默认方法或多个static方法),该接口称为函数式接口。函数式接口其存在的意义,主要是配合Lambda 表达式 来使用。

如例所示,AD接口只有一个adAttack方法,那么就可以被注解为@FunctionalInterface,而AP接口有两个方法apAttack()和apAttack2(),那么就不能被注解为函数式接口。

AD.java

1
2
3
4
5
6
package annotation;

@FunctionalInterface
public interface AD {
public void adAttack();
}

AP.java

1
2
3
4
5
6
7
package annotation;

@FunctionalInterface
public interface AP {
public void apAttack();
public void apAttack2();
}

自定义注解

在本例中,把数据库连接的工具类DBUtil改造成为注解的方式,来举例演示怎么自定义注解以及如何解析这些自定义注解。

非注解方式DBUtil

通常来讲,在一个基于JDBC开发的项目里,都会有一个DBUtil这么一个类,在这个类里统一提供连接数据库的IP地址,端口,数据库名称, 账号,密码,编码方式等信息。如例所示,在这个DBUtil类里,这些信息,就是以属性的方式定义在类里的。

大家可以运行试试,运行结果是获取一个连接数据库test的连接Connection实例。

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
package util;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class DBUtil {
static String ip = "127.0.0.1";
static int port = 3306;
static String database = "test";
static String encoding = "UTF-8";
static String loginName = "root";
static String password = "admin";
static{
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

public static Connection getConnection() throws SQLException {
String url = String.format("jdbc:mysql://%s:%d/%s?characterEncoding=%s", ip, port, database, encoding);
return DriverManager.getConnection(url, loginName, password);
}
public static void main(String[] args) throws SQLException {
System.out.println(getConnection());
}
}

自定义注解@JDBCConfig

接下来,就要把DBUtil这个类改造成为支持自定义注解的方式。 首先创建一个注解JDBCConfig

1.创建注解类型的时候即不使用class也不使用interface,而是使用@interface

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
public @interface JDBCConfig 
```

> 2.元注解
`@Target({METHOD,TYPE})` 表示这个注解可以用用在类/接口上,还可以用在方法上
`@Retention(RetentionPolicy.RUNTIME)` 表示这是一个运行时注解,即运行起来之后,才获取注解中的相关信息,而不像基本注解如@Override 那种不用运行,在编译时eclipse就可以进行相关工作的编译时注解。
`@Inherited` 表示这个注解可以被子类继承
`@Documented` 表示当执行javadoc的时候,本注解会生成相关文档
请在学习完本知识点最后一个步骤解析注解之后,再查看 元注解,做更详尽的学习。

> 3.注解元素,这些注解元素就用于存放注解信息,在解析的时候获取出来

String ip();
int port() default 3306;
String database();
String encoding();
String loginName();
String password();

```java
package anno;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;

import java.lang.annotation.Documented;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({METHOD,TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface JDBCConfig {
String ip();
int port() default 3306;
String database();
String encoding();
String loginName();
String password();
}

注解方式DBUtil

有了自定义注解@JDBCConfig之后,我们就把非注解方式DBUtil改造成为注解方式DBUtil。
如例所示,数据库相关配置信息本来是以属性的方式存放的,现在改为了以注解的方式,提供这些信息了。

注: 目前只是以注解的方式提供这些信息,但是还没有解析,接下来进行解析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package util;

import anno.JDBCConfig;

@JDBCConfig(ip = "127.0.0.1", database = "test", encoding = "UTF-8", loginName = "root", password = "admin")
public class DBUtil {
static {
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

}

解析注解

接下来就通过反射,获取这个DBUtil这个类上的注解对象

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
JDBCConfig config = DBUtil.class.getAnnotation(JDBCConfig.class);
```

> 拿到注解对象之后,通过其方法,获取各个注解元素的值:
```
String ip = config.ip();
int port = config.port();
String database = config.database();
String encoding = config.encoding();
String loginName = config.loginName();
String password = config.password();
```

> 后续就一样了,根据这些配置信息得到一个数据库连接Connection实例。

```java
package util;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

import anno.JDBCConfig;

@JDBCConfig(ip = "127.0.0.1", database = "test", encoding = "UTF-8", loginName = "root", password = "admin")
public class DBUtil {
static {
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

public static Connection getConnection() throws SQLException, NoSuchMethodException, SecurityException {
JDBCConfig config = DBUtil.class.getAnnotation(JDBCConfig.class);

String ip = config.ip();
int port = config.port();
String database = config.database();
String encoding = config.encoding();
String loginName = config.loginName();
String password = config.password();

String url = String.format("jdbc:mysql://%s:%d/%s?characterEncoding=%s", ip, port, database, encoding);
return DriverManager.getConnection(url, loginName, password);
}

public static void main(String[] args) throws NoSuchMethodException, SecurityException, SQLException {
Connection c = getConnection();
System.out.println(c);
}
}

元注解

元注解概念

在讲解元注解概念之前,我们先建立元数据的概念。 元数据在英语中对应单词 metadata, metadata在wiki中的解释是:
Metadata is data [information] that provides information about other data
为其他数据提供信息的数据

这样元注解就好理解了,元注解 meta annotation用于注解 自定义注解 的注解。

元注解有这么几种:

1
2
3
4
5
@Target 
@Retention
@Inherited
@Documented
@Repeatable (java1.8 新增)

@Target

@Target 表示这个注解能放在什么位置上,是只能放在类上?还是即可以放在方法上,又可以放在属性上。自定义注解@JDBCConfig 这个注解上的@Target是:@Target({METHOD,TYPE}),表示他可以用在方法和类型上(类和接口),但是不能放在属性等其他位置。 可以选择的位置列表如下:

1
2
3
4
5
6
7
8
ElementType.TYPE:能修饰类、接口或枚举类型
ElementType.FIELD:能修饰成员变量
ElementType.METHOD:能修饰方法
ElementType.PARAMETER:能修饰参数
ElementType.CONSTRUCTOR:能修饰构造器
ElementType.LOCAL_VARIABLE:能修饰局部变量
ElementType.ANNOTATION_TYPE:能修饰注解
ElementType.PACKAGE:能修饰包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package anno;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;

import java.lang.annotation.Documented;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({METHOD,TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface JDBCConfig {
String ip();
int port() default 3306;
String database();
String encoding();
String loginName();
String password();
}

@Retention

@Retention 表示生命周期,自定义注解@JDBCConfig 上的值是 RetentionPolicy.RUNTIME, 表示可以在运行的时候依然可以使用。

@Retention可选的值有3个:

  • RetentionPolicy.SOURCE: 注解只在源代码中存在,编译成class之后,就没了。@Override 就是这种注解。
  • RetentionPolicy.CLASS: 注解在java文件编程成.class文件后,依然存在,但是运行起来后就没了。@Retention的默认值,即当没有显式指定@Retention的时候,就会是这种类型。
  • RetentionPolicy.RUNTIME: 注解在运行起来之后依然存在,程序可以通过反射获取这些信息,自定义注解@JDBCConfig 就是这样。

大家可以试试把自定义注解@JDBCConfig@Retention改成其他两种,并且运行起来,看看有什么不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package anno;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;

import java.lang.annotation.Documented;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({METHOD,TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented

public @interface JDBCConfig {
String ip();
int port() default 3306;
String database();
String encoding();
String loginName();
String password();
}

@Inherited

@Inherited 表示该注解具有继承性。如例,设计一个DBUtil的子类,其getConnection2方法,可以获取到父类DBUtil上的注解信息。

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
package util;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

import anno.JDBCConfig;

public class DBUtilChild extends DBUtil {

public static Connection getConnection2() throws SQLException, NoSuchMethodException, SecurityException {
JDBCConfig config = DBUtilChild.class.getAnnotation(JDBCConfig.class);
String ip = config.ip();
int port = config.port();
String database = config.database();
String encoding = config.encoding();
String loginName = config.loginName();
String password = config.password();

String url = String.format("jdbc:mysql://%s:%d/%s?characterEncoding=%s", ip, port, database, encoding);
return DriverManager.getConnection(url, loginName, password);
}

public static void main(String[] args) throws NoSuchMethodException, SecurityException, SQLException {
Connection c = getConnection2();
System.out.println(c);
}
}

@Documented

@Documented 如图所示, 在用javadoc命令生成API文档后,DBUtil的文档里会出现该注解说明。

注: 使用eclipse把项目中的.java文件导成API文档步骤:

  1. 选中项目
  2. 点开菜单File
  3. 点击Export
  4. 点开java->javadoc->点next
  5. 点finish

@Repeatable (java1.8 新增)

当没有@Repeatable修饰的时候,注解在同一个位置,只能出现一次,如例所示:

1
2
@JDBCConfig(ip = "127.0.0.1", database = "test", encoding = "UTF-8", loginName = "root", password = "admin")
@JDBCConfig(ip = "127.0.0.1", database = "test", encoding = "UTF-8", loginName = "root", password = "admin")

重复做两次就会报错了。
使用@Repeatable之后,再配合一些其他动作,就可以在同一个地方使用多次了。

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
package util;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

import anno.JDBCConfig;

@JDBCConfig(ip = "127.0.0.1", database = "test", encoding = "UTF-8", loginName = "root", password = "admin")
@JDBCConfig(ip = "127.0.0.1", database = "test", encoding = "UTF-8", loginName = "root", password = "admin")
public class DBUtil {
static {
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

public static Connection getConnection() throws SQLException, NoSuchMethodException, SecurityException {
JDBCConfig config = DBUtil.class.getAnnotation(JDBCConfig.class);
System.out.println(config);

String ip = config.ip();
int port = config.port();
String database = config.database();
String encoding = config.encoding();
String loginName = config.loginName();
String password = config.password();

String url = String.format("jdbc:mysql://%s:%d/%s?characterEncoding=%s", ip, port, database, encoding);
return DriverManager.getConnection(url, loginName, password);
}

public static void main(String[] args) throws NoSuchMethodException, SecurityException, SQLException {
Connection c = getConnection();
System.out.println(c);
}
}

@Repeatable 运用举例

比如在练习练习-查找文件内容 中有一个要求,即查找文件后缀名是.java的文件,我们把部分代码修改为注解,并且使用@Repeatable 这个元注解来表示,文件后缀名的范围可以是java, html, css, js 等等。

为了紧凑起见,把注解作为内部类的形式放在一个文件里。

  1. 注解FileTypes,其value()返回一个FileType数组
  2. 注解FileType,其@Repeatable的值采用FileTypes
  3. 运用注解:在work方法上重复使用多次@FileType注解
  4. 解析注解: 在work方法内,通过反射获取到本方法上的FileType类型的注解数组,然后遍历本数组。
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
package annotation;
import static java.lang.annotation.ElementType.METHOD;

import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

public class FindFiles {
@Target( METHOD)
@Retention( RetentionPolicy.RUNTIME )
public @interface FileTypes {
FileType[] value();
}

@Target( METHOD )
@Retention( RetentionPolicy.RUNTIME )
@Repeatable( FileTypes.class )
public @interface FileType {
String value();
};

@FileType( ".java" )
@FileType( ".html" )
@FileType( ".css" )
@FileType( ".js" )
public void work(){

try {
FileType[] fileTypes= this.getClass().getMethod("work").getAnnotationsByType(FileType.class);
System.out.println("将从如下后缀名的文件中查找文件内容");
for (FileType fileType : fileTypes) {
System.out.println(fileType.value());
}
System.out.println("查找过程略。。。");
} catch (NoSuchMethodException | SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

public static void main(String[] args) {
new FindFiles().work();
}
}

仿 Hibernate 注解

hibernate两种配置方式

hibernate有两种配置方式,分别是*.hbm.xml 配置方式 和注解方式。 虽然方式不一样,但是都是用于解决如下问题:

  1. 当前类是否实体类
  2. 对应的表名称
  3. 主键对应哪个属性, 自增长策略是什么,对应字段名称是什么
  4. 非主键属性对应字段名称是什么

接下来,我会做一套仿hibernate的注解,并且在一个实体类Hero上运用这些注解,并通过反射解析这些注解信息,来解决上述的问题。

自定义hibernate注解

参考hibernate的 注解配置方式 ,自定义5个注解,分别对应hibernate中用到的注解:

1
2
3
4
5
hibernate_annotation.MyEntity 对应 javax.persistence.Entity
hibernate_annotation.MyTable 对应 javax.persistence.Table
hibernate_annotation.MyId 对应 javax.persistence.Id
hibernate_annotation.MyGeneratedValue 对应 javax.persistence.GeneratedValue
hibernate_annotation.MyColumn 对应 javax.persistence.Column

MyEntity.java

1
2
3
4
5
6
7
8
9
10
11
12
package hibernate_annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyEntity {

}

MyTable.java

1
2
3
4
5
6
7
8
9
10
11
12
13
package hibernate_annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTable {

String name();
}

MyId.java

1
2
3
4
5
6
7
8
9
10
11
12
package hibernate_annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyId {

}

MyGeneratedValue.java

1
2
3
4
5
6
7
8
9
10
11
12
package hibernate_annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyGeneratedValue {
String strategy();
}

MyColumn.java

1
2
3
4
5
6
7
8
9
10
11
12
package hibernate_annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyColumn {
String value();
}

运用在Hero对象上

像以注解方式配置Product类 那样,在Hero类上运用这些自定义注解:
当注解的方法是value的时候,给这个注解赋值时,本来应该是:

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
@MyColumn(value="name_")
```


> 现在可以简略一点,写为:


```java
@MyColumn("name_")
```


> 只有当名称是value的时候可以这样,其他名称如name, strategy等不行。

```java
package pojo;

import hibernate_annotation.MyColumn;
import hibernate_annotation.MyEntity;
import hibernate_annotation.MyGeneratedValue;
import hibernate_annotation.MyId;
import hibernate_annotation.MyTable;

@MyEntity
@MyTable(name="hero_")
public class Hero {
private int id;
private String name;
private int damage;
private int armor;

@MyId
@MyGeneratedValue(strategy = "identity")
@MyColumn("id_")
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
@MyColumn("name_")
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@MyColumn("damage_")
public int getDamage() {
return damage;
}
public void setDamage(int damage) {
this.damage = damage;
}
@MyColumn("armor_")
public int getArmor() {
return armor;
}
public void setArmor(int armor) {
this.armor = armor;
}

}

解析注解

创建一个解析类ParseHibernateAnnotation,获取Hero类上配置的注解信息,其运行结果如图所示。

思路如下:

  1. 首先获取Hero.class类对象;
  2. 判断本类是否进行了MyEntity 注解;
  3. 获取注解 MyTable
  4. 遍历所有的方法,如果某个方法有MyId注解,那么就记录为主键方法primaryKeyMethod
  5. 把主键方法的自增长策略注解MyGeneratedValue和对应的字段注解MyColumn 取出来,并打印;
  6. 遍历所有非主键方法,并且有MyColumn注解的方法,打印属性名称和字段名称的对应关系;

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
67
68
69
70
71
72
package test;

import java.lang.reflect.Method;

import hibernate_annotation.MyColumn;
import hibernate_annotation.MyEntity;
import hibernate_annotation.MyGeneratedValue;
import hibernate_annotation.MyId;
import hibernate_annotation.MyTable;
import pojo.Hero;

public class ParseHibernateAnnotation {

public static void main(String[] args) {

Class<Hero> clazz = Hero.class;
MyEntity myEntity = (MyEntity) clazz.getAnnotation(MyEntity.class);
if (null == myEntity) {
System.out.println("Hero类不是实体类");
} else {
System.out.println("Hero类是实体类");
MyTable myTable= (MyTable) clazz.getAnnotation(MyTable.class);
String tableName = myTable.name();
System.out.println("其对应的表名是:" + tableName);
Method[] methods =clazz.getMethods();
Method primaryKeyMethod = null;
for (Method m: methods) {
MyId myId = m.getAnnotation(MyId.class);
if(null!=myId){
primaryKeyMethod = m;
break;
}
}

if(null!=primaryKeyMethod){
System.out.println("找到主键:" + method2attribute( primaryKeyMethod.getName() ));
MyGeneratedValue myGeneratedValue =
primaryKeyMethod.getAnnotation(MyGeneratedValue.class);
System.out.println("其自增长策略是:" +myGeneratedValue.strategy());
MyColumn myColumn = primaryKeyMethod.getAnnotation(MyColumn.class);
System.out.println("对应数据库中的字段是:" +myColumn.value());
}
System.out.println("其他非主键属性分别对应的数据库字段如下:");
for (Method m: methods) {
if(m==primaryKeyMethod){
continue;
}
MyColumn myColumn = m.getAnnotation(MyColumn.class);
//那些setter方法上是没有MyColumn注解的
if(null==myColumn)
continue;
System.out.format("属性: %s\t对应的数据库字段是:%s%n",method2attribute(m.getName()),myColumn.value());

}

}

}

private static String method2attribute(String methodName) {
String result = methodName; ;
result = result.replaceFirst("get", "");
result = result.replaceFirst("is", "");
if(result.length()<=1){
return result.toLowerCase();
}
else{
return result.substring(0,1).toLowerCase() + result.substring(1,result.length());
}

}
}

注解分类

按照作用域分

根据注解的作用域@Retention,注解分为:

1
2
3
RetentionPolicy.SOURCE: Java源文件上的注解
RetentionPolicy.CLASS: Class类文件上的注解
RetentionPolicy.RUNTIME: 运行时的注解

按照来源分

按照注解的来源,也是分为3类

  1. 内置注解 如@Override ,@Deprecated 等等
  2. 第三方注解,如Hibernate, Struts等等
  3. 自定义注解,如仿hibernate的自定义注解

在工作中,大部分都是使用第三方注解, 当然第三方注解本身就是自定义注解。 本教程的主要作用是帮助大家理解这些第三方注解是如何工作的,让大家用得心里踏实一些。

王方钢 / Kenny Wang wechat
0%