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码以及对应的十进制和十六进制数字,不可见的暂未列出。

字符十进制数字十六进制数字
!3321
3422
#3523
$3624
%3725
&3826
3927
(4028
)4129
*422A
+432B
,442C
-452D
.462E
/472F
04830
14931
25032
35133
45234
55335
65436
75537
85638
95739
:583A
;593B
<603C
=613D
>623E
@6440
A6541
B6642
C6743
D6844
E6945
F7046
G7147
H7248
I7349
J744A
K754B
L764C
M774D
N784E
O794F
P8050
Q8151
R8252
S8353
T8454
U8555
V8656
W8757
X8858
Y8959
Z905A
[915B
\925C
]935D
^945E
_955F
`9660
a9761
b9862
c9963
d10064
e10165
f10266
g10367
h10468
i10569
j1066A
k1076B
l1086C
m1096D
n1106E
o1116F
p11270
q11371
r11472
s11573
t11674
u11775
v11876
w11977
x12078
y12179
z1227A
{1237B
\1247C
}1257D
~1267E

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

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);

}

}