0%

一.Lambda表达式
1.1 函数式编程的思想
尽量简单的格式简化面对对象的复杂语法。
面对对象是以何种形式去做,而函数式编程思想强调是拿什么东西做什么事,而不是强调以何种形式去做。

1.2 冗余的Runnable代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestRUNNABLEDEMO {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"RUNNABLE");
}
});

t1.start();

//为了避免创建一个新类,不得不创建匿名内部类
//为了迎合面向对象的语法,只能使用Runnable的实现类
//重写时,方法必须和接口中一模一样
//真正需要的其实就是任务代码
}
}

1.3 函数式编程Lambda的体验

1
2
3
4
5
Thread t2 =new Thread(()->{
System.out.println(Thread.currentThread().getName()+"RUNNABLE");
});

t2.start();

1.4 Lambda标准格式介绍
(参数列表) -> {方法体; return 返回值;}

详情介绍:
| | |
|———-|:———————————————————————-:|
| (参数列表) | 相当于方法的参数,如果没有参数,那么只写小括号即可(),注意小括号不能省略 |
| -> | 固定用法,代码拿着前面的参数,做什么事 |
| {} | 大括号中先写计算过程,如果有返回值return返回值;如果没有返回值,return语句可以省略 |

1.5 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
54
55
56
57
58
59
60
61
62

public class Test_ComparatorDemo {
public static void main(String[] args) {
Integer[] num = {4, 5, 61, 7, 8, 9, 34, 56, 345};
Arrays.sort(num);
System.out.println(Arrays.toString(num));

Arrays.sort(num, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
});
System.out.println(Arrays.toString(num));

public class Test_ComparatorDemo {
public static void main(String[] args) {
Integer[] num = {4, 5, 61, 7, 8, 9, 34, 56, 345};
//使用Lambda表达式改写上方冗余代码
Arrays.sort(num, (Integer o1, Integer o2) -> {
return o2 - o1;
});
System.out.println(Arrays.toString(num));
}
}

public class Test_ComparatorDemo {
public static void main(String[] args) {
Dog[] dog = new Dog[4];
dog[0] = new Dog(1, "a", 2);
dog[1] = new Dog(2, "b", 2);
dog[2] = new Dog(3, "c", 3);
dog[3] = new Dog(4, "d", 4);
Arrays.sort(dog, new Comparator<Dog>() {
@Override
public int compare(Dog o1, Dog o2) {
return o1.age - o2.age;
}
});

for (Dog d : dog) {
System.out.println(d);
}

System.out.println();
}


public class Test_ComparatorDemo {
public static void main(String[] args) {
Dog[] dog = new Dog[4];
dog[0] = new Dog(1, "a", 2);
dog[1] = new Dog(2, "b", 2);
dog[2] = new Dog(3, "c", 3);
dog[3] = new Dog(4, "d", 4);

Arrays.sort(dog,(Dog o1,Dog o2)->{return o2.age-o1.age;});
for (Dog d : dog) {
System.out.println(d);
}
}
}

1.6 Lambda的省略格式
a.参数类型可以省略
b.如果参数只有一个,那么小括号可以省略
c.如果{}中的代码可以写成一句代码,那么”{}”,”return”关键字和”;”可以同时省略(不能只省略一部分)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
new Thread(()->{System.out.println(Thread.currentThread().getName()+"RUNNABLE");}).start();

new Thread(()->System.out.println(Thread.currentThread().getName()+"RUNNABLE")).start();

Arrays.sort(num, (Integer o1, Integer o2) -> {return o2 - o1;});

Arrays.sort(num, (o1, o2) -> o2 - o1);


Arrays.sort(dog, (Dog o1, Dog o2) -> {
return o2.age - o1.age;
});

Arrays.sort(dog, (o1, o2) -> o2.age - o1.age);

1.7 强烈注意:Lambda的使用前提
a.Lambda只能用于替换有且仅有一个抽象方法的接口的匿名内部类对象,这种接口称为函数式接口
b.Lambda具有上下午推断的功能,所以才会出现Lambda的省略格式

二.Stream流
2.1 引入:传统的集合操作

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 TestDemo {
public static void main(String[] args) {
List<String > list = new ArrayList<String >();
Collections.addAll(list,"zaa","wdt","xcs","zd","lf");

ArrayList<String > z = new ArrayList<String>();
for (String name:list){
if(name.startsWith("z")){
z.add(name);
}
}

ArrayList<String > Three = new ArrayList<String>();
for (String name:list){
if(name.length()==3){
Three.add(name);
}
}

System.out.println(z);
System.out.println(Three);
}
}

2.2 循环遍历的弊端分析
Lambda注重于做什么,传统的面向对象注重于怎么做

1
2
3
4
5
for (String name:list){
if(name.startsWith("z")){
z.add(name);
}
}

为了解决面向对象的语法复杂形式,引入了一种新的技术:Stream 流式思想

2.3 Stream的优雅写法

1
list.stream().filter(s -> s.startsWith(“x”)).filter(s -> s.length() == 3).forEach(s -> System.out.println(s));
Stream流的使用
•生成流
通过数据源(集合,数组等)生成流 list.stream();
•中间操作
•终结操作
一个流只能有一个终结操作,当这个操作执行完后,无法再被操作。

2.4 流式思想的概述

2.5 三种获取流的方式
a.Collection集合获取流
Stream s = 集合对象.stream();

b.Map集合不能直接获取流,但是可以间接获取流
map.keySet().stream();获取map的键
map.values().stream();获取map的值流
map.entrySet().stream();获取map的键值对流

c.数组获取流
Stream<数组中元素的类型> A =Stream.of(数据类型…变量名);

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

public static void main(String[] args) {
//获取各种容器的流
//单列集合
ArrayList<String> list = new ArrayList<String >();
Stream<String > s1 = list.stream();
HashMap<String ,Integer> map = new HashMap<String, Integer>(16);
//键流
Stream<String> keyStream = map.keySet().stream();
//值流
Stream<Integer> valueStream = map.values().stream();
//键值对流
Stream<Map.Entry<String,Integer>> entryStream = map.entrySet().stream();

Integer[] arr = {10,20,30,40};
Stream<Integer> num = Stream.of(arr);
Stream<Integer> nums = Stream.of(11,22,33,44);
}

2.6 Stream流中的常用方法
•逐个处理:forEach

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

public class StreamDemo02 {
public static void main(String[] args) {
Stream<Integer> s1 = Stream.of(11, 22, 33, 44);
// s1.forEach(new Consumer<Integer>() {
// @Override
// public void accept(Integer integer) {
// System.out.println(integer);
// }
// });
s1.forEach(integer -> System.out.println(integer));
}
}

•统计个数:count

1
2
3
4
5
public static void main(String[] args) {
Stream<Integer> s1 = Stream.of(11, 22, 33, 44);
long count = s1.count();
System.out.println(count);
}

•过滤:filter

1
2
3
4
5
6
7
8
9
10
11
//        s1.filter(new Predicate<Integer>() {
// @Override
// public boolean test(Integer integer) {
//// if (integer>100000) {
//// return true;
//// }
// return integer>1000;
// }
// });
Stream<Integer> integerStream = s1.filter(integer -> integer > 1000);
integerStream.forEach( );

•取前几个:limit

1
2
3
Stream<Integer> limit = s1.limit(3);
limit.forEach(System.out::println);

•跳过前几个:skip

1
2
Stream<Integer> skip = s1.skip(2);
skip.forEach(System.out::println);

•映射方法:map

1
2
3
4
5
6
7
8
9

// Stream<String> str = s6.map(new Function<Integer, String>() {
// @Override
// public String apply(Integer integer) {
// return String.valueOf(integer);
// }
// });
Stream<String> str = s6.map(s -> String.valueOf(s));
str.forEach(s -> System.out.println(s));

mapToInt(ToIntFunction mapper):返回一个IntStream其中包含将给定函数应用于此流的结果

1
2
int result = list.stream().mapToInt(Integer::parseInt).sum;
System.out.println(result);

•静态方式合并流:concat

1
2
3
4
5
6
7
public static Stream<T> concat(Stream<T> s1,Stream<T> s2);

Stream<String > s7 = Stream.of("a","b");
Stream<String > s8 = Stream.of("c","d");

Stream<String > ss = Stream.concat(s7, s8);
ss.forEach(s-> System.out.println(s));

•静态方式合并流:distinct –返回该流的不同元素(根据Object.equals(Object))组成的流

1
2
3
4
5
6
public static Stream<T> concat(Stream<T> s1,Stream<T> s2);

Stream<String > s7 = Stream.of("a","b","d");
Stream<String > s8 = Stream.of("c","d","a");

Stream<String > ss = Stream.concat(s7, s8).distinct().forEach(System.out::println);

•sorted():返回由此流的元素组成的流,根据自然顺序排序

1
2
3
4
5
6
7
8
9
10
public static Stream<T> concat(Stream<T> s1,Stream<T> s2);

Stream<String > s7 = Stream.of("a","b");
Stream<String > s8 = Stream.of("c","d");

Stream<String > ss = Stream.concat(s7, s8).sorted((s1,s2)->{
int num = s1.len()-s2.length();
int num2 = num==0?s1.compareTo(s2):sum;
return sum2;
}).forEach(System.out::println);

•sorted(Comparator comparator):返回由该流的元素组成的流,根据提供的comparator进行排序

注意:
a.如果是两个以上流的合并,需要多次两两合并
有filter,limit,skip,map,concat

b.如果两个流的泛型不一致也可以合并,合并之后新流的泛型是他们的共同父类
有forEach,count

2.7 练习:集合元素的处理(Stream方式)

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

public class StreamDemo04_Lambda {
public static void main(String[] args) {
List<String> s1 = new ArrayList<String>();
List<String> s2 = new ArrayList<String>();
Collections.addAll(s1, "aa", "abc", "abcd");
Collections.addAll(s2, "ba", "cd", "ccc", "abcd");

Stream<String> a = s1.stream().filter(s -> s.length() == 3).limit(3);

Stream<String> b = s2.stream().filter(s -> s.startsWith("a")).skip(2);

Stream<String> ss = Stream.concat(a, b);

Stream<Person> personStream = ss.map(s -> new Person(s));

personStream.forEach(System.out::println);

}
}

2.8 总结:函数拼接和终结方法
函数拼接方法:
调用该方法之后,返回还是一个流对象。
由于这种方法返回的还是流对象,因此支持链式编程

终结方法:
调用该方法之后,返回值不是流或无返回值
由于终结方法返回的不是流对象,因此不支持链式编程,并且当某个流调用终结方法之后,该流就关闭了,不能继续调用其他方法

2.9 收集Stream的结果

可以把流收集到集合中:调用流的collect方法即可
可以把流收集到数组中:调用toArray()方法即可

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

public class StreamDemo03 {
public static void main(String[] args) {
Stream<Integer> s1 = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> s2 = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> s3 = Stream.of(1, 2, 3, 4, 5);
//收集到集合
List<Integer> collect = s1.collect(Collectors.toList());
System.out.println(collect);

//收集到Set
Set<Integer> collect1 = s2.collect(Collectors.toSet());
System.out.println(collect1);

//收集到数组
Object[] objects = s3.toArray();

for(Object obj:objects){
System.out.println(obj);
}
}
}

注意:
a.一个流只能收集一次(第二次会报错)
b.如果收集到数组,默认是Object数组;

总结:
1.Lambda
标准格式:(参数列表)->{方法体;return 返回值;}
省略格式:
参数类型可省略
只有一个参数,小括号可省略
如果{}中只有一句代码,那么{} ; return可省略
2.Stream流
a.集合或者数组获取流的方法
Stream s = 集合.stream();//单列集合
Stream s = Stream.of(数组/可变参数); //数组获取流
b.调用流的各种方式
filter limit skip map count foreach concat

一.File类
1.1 File类的作用
File类可以表示文件或文件夹,是文件和目录的抽象表示
•文件和目录是可以通过File封装成对象的
•对于File而言,其封装的并不是一个真正存在的文件,仅仅是一个路径名。将来通过具体的操作把这个路径转换为具体存在的。

1.2 File类的构造

方法名 说明
public File(String path); 通过将给定的路径名字符串转换为抽象路径名来创建新的File实例
public File(String parent,String child); 从父路径名字符串和子路径名字符串创建新的File实例
public File(File parent,String child); 从父抽象路径名和子路径名字符串创建新的File实例

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

public class FileDemo01 {
public static void main(String[] args) {
File f1 = new File("/Users/u/Desktop/12306Bypass/Logs/2019-01-03.txt");

System.out.println(f1);

File f2 = new File("/Users/u/Desktop","12306Bypass/Logs/2019-01-03.txt");
System.out.println(f2);

File parent = new File("/Users/u/Desktop");
File f3 = new File(parent,"12306Bypass/Logs/2019-01-03.txt");
System.out.println(f3);
}
}

1.3 File类的获取方法
public String getAbsolutePath();//获取该File对象的绝对路径
public String getPath();//获取该File对象构造时,传入的对象
public String getName();//获取该File对象的代表的文件或文件夹的名字
public long length();//获取该File对象的文件字节

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

public class FileDemo02 {
public static void main(String[] args) {
File f1 = new File("~/Desktop/12306Bypass/Logs/2019-01-03.txt");
//getAbsolutePath
String absolutePath = f1.getAbsolutePath();
System.out.println(absolutePath);

//getPath
String path = f1.getPath();
System.out.println(path);

//name
String name = f1.getName();
System.out.println(name);

//length
long length = f1.length();
System.out.println(length+" byte");

}
}

注意:length方法只能获取文件大小,不能获取文件夹大小

1.4 相对路径和绝对路径
绝对路径:以盘符开头的路径,一个完整的路径
例如:/Users/u/Desktop”,”12306Bypass/Logs/2019-01-03.txt

相对路径:以当前项目的根目录为起始的路径

1
2
3
4
5
6
7
8
9
10

public class FileDemo03 {
public static void main(String[] args) {
File f1 = new File("/Users/u/Desktop/12306Bypass/Logs/2019-01-03.txt");
File f2 = new File("2019-01-04.txt");

String absolutePath = f2.getAbsolutePath();
System.out.println(absolutePath);
}
}

1.5 File类的判断方法
•public boolean exists();//判断该File对象代表的文件和文件夹是否存在
•public boolean isDirectory();//判断该File对象代表的是否是文件夹
•public boolean isFile();//判断该File对象代表的是否是文件

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

public class FileDemo04 {
public static void main(String[] args) {
File file = new File("/Users/u/Desktop/12306Bypass/Logs/2019-01-03.txt");
boolean b = file.exists();
System.out.println(b);

boolean directory = file.isDirectory();
System.out.println(directory);

boolean file1 = file.isFile();
System.out.println(file1);
}

}
1.6 File类的创建删除方法
public boolean mkdrir();//创建单级文件夹,返回值表示是否成功
public boolean mkdrirs();//创建多级文件夹,返回值表示是否成功

public boolean createNewFile();//创建文件,返回值表示是否成功

public boolean delete();//删除该File对象或文件夹,返回值表示是否成功

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

public class FileDemo05 {
public static void main(String[] args) throws IOException {
File file = new File("2.txt");
boolean newFile = file.createNewFile();
System.out.println(newFile);
boolean mkdir = file.mkdir();
System.out.println(mkdir);

File file2 = new File("a/b/c/2.txt");
boolean mkdirs = file2.mkdirs();
System.out.println(mkdirs);

File file1 = new File("2.txt");
boolean delete = file1.delete();
System.out.println(delete);

File file3 = new File("a/b/c/2.txt");
boolean delete1 = file3.delete();
System.out.println(delete1);
File file4 = new File("a/b/c/2.txt");
boolean delete2 = file4.delete();
System.out.println(delete2);

}
}

注意:
a.mkdir 和 mkdirs的区别
b.delete方法要么删文件,要么删文件夹,不能删除空文件夹

1.7 File类的遍历目录方法
public String[] list();//列出当前文件夹下所有直接的文件和文件夹的名字
public File[] listFiles();//列出当前文件夹下所有直接的文件和文件夹的File对象【重要】

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

public class FileDemo06 {
public static void main(String[] args) {
File file = new File("a");
String[] list = file.list();
for (String filename : list
) {
System.out.println(filename);
}
File[] files = file.listFiles();
for (File filearr : files) {
System.out.println(filearr);
}
}
}

注意:list和listFiles只能列出直接的子文件或子文件夹

二.递归
2.1 什么是递归
递归不是Java语言独有(基本上所有语言都可以递归)
递归就是:在方法中调用该方法本身

1
2
3
4
5
6
7
8
9
10
public class Recurrence {
public static void main(String[] args) {
method();
}
public static void method() {
System.out.println("method");
//调用自己
method();
}
}

无限递归(死递归)出现这个错误 StackOverflowError 栈溢出错误

如果要使用递归,必须保证递归有出口(结束条件)

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

public class Recurrence {
public static void main(String[] args) {
method1(10);
}
//有出口递归
public static void method1(int n) {
System.out.println("method1 "+n);
//调用自己
if(n == 0){
return;//递归出口
}
method1(n-1);

}
// public static void method() {
// System.out.println("method");
// //调用自己
// method();
// }
}

注意:就算递归有出口,也要保证递归在运行到出口之前次数也不能太多(太多也会爆栈)

使用递归三大步骤:
a.先定义一个方法
b.找规律调用自己
c.让递归有出口(结束条件)

2.2 递归求和案例
测试案例:求1-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

public class Recurrence02 {
public static void main(String[] args) {
int sum = getSum(10);
System.out.println(sum);
int Recurrence_getSum = Recurrence_getSum(100);
System.out.println(Recurrence_getSum);
}

//使用递归
public static int Recurrence_getSum(int n) {
if (n == 1) {
return 1;
}
return Recurrence_getSum(n - 1) + n;
}

//使用循环
public static int getSum(int n) {
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
return sum;
}
}

2.3 递归求阶乘案例

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

public class Recurrence03_getFactorial {
public static void main(String[] args) {
int sum = getFactorial(10);
System.out.println(sum);
int recurrenceGetSum = Recurrence_getFactorial(10);
System.out.println(recurrenceGetSum);
}

//使用递归
public static int Recurrence_getFactorial(int n) {
if (n == 1) {
return 1;
}
return Recurrence_getFactorial(n - 1) * n;
}

//使用循环
public static int getFactorial(int n) {
int Factorial = 1;
for (int i = 1; i <= n; i++) {
Factorial *= i;
}
return Factorial;
}
}

2.4 文件搜索案例

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

public class Recurrence04_File {
public static void main(String[] args) {
File fileDir = new File("/Users/u/Downloads/a/b");
findTxtFile(fileDir);
}

public static void findTxtFile(File file) {
File[] files = file.listFiles();

for (File filearr : files) {
if (filearr.getName().endsWith(".java") && filearr.isFile()) {
System.out.println(filearr);
}else if(filearr.isDirectory()){
findTxtFile(filearr);
}
}
}
}

三.IO流的概述
3.1 什么是IO流
I:Input输入流,数据从外部设备到程序中,”读流”
O:Outpur输出流,数据从程序写到外部设备,”写流”
流:一种抽象概念,数据传输的总称。数据在设备间的传输称为流,流的本质是数据传输

Java中流的流向都是以内存角度而言的

3.2 IO流的分类
a.根据流的方向分类
输入流:读数据(Input–>Read)
输出流:写数据(Output–>Write)

b.根据流中的数据类型分类
字节流:byte
字符流:char

以上两种分类可以综和为四大类
字节输入流,字节输出流,字符输入流,字符输出流

3.3 Java中的四大IO流(其他流都是这四个之一的子类)
字节输入流:InputStream(顶层父类,抽象类)
字节输出流:OutputStream(顶层父类,抽象类)

字符输入流:Reader(顶层父类,抽象类)
字符输出流:Writer(顶层父类,抽象类)

技巧:Java中所有的流,都会是以上四个流中某一个的子类,且具体的流的命名是非常有规范的:功能名+父类名
如:
FileWriter,向文件中写出字符为单位的数据
FileInputStream,从文件中读取以字节为单位的数据

四.字节流
4.1 万物皆对象和IO流一切皆字节
万物皆字节对象:现实生活中的任何东西,我们在Java中都可以使用一个对象表示
IO流中的一切皆字节:电脑所有的数据,最终都是由字节组成的(二进制)

4.2 字节输出流
顶层父类(抽象类)
共性方法:
public void close();//关闭该流,释放资源
public void flush();//刷新缓冲区(主要字符流使用)

public void write(int b);//一次写一个字节,输入是int,但是只能写一个byte的大小,即最大127
public void write(byte[] bs);//一次写一个字节数组
public void write(byte[] bs,int startIndex,int len);//一次写这一个字节数组中的一部分

4.4 FileOutputStream类的使用
文件的字节输出流(向文件中写字节数据)

•构造方法
public FileOutputStream(String path);//必须传入文件的路径
public FileOutputStream(File file);//必须传入文件的File对象

public FileOutputStream(String path);//必须传入文件的路径

1
2
3
4
5
6
7
8
9
10
11

public class Demo08_FileOutputStream {
public static void main(String[] args) throws FileNotFoundException {
FileOutputStream fos = new FileOutputStream("1.txt");
/*
创建文件
判断文件是否存在,如果存在,则清空该文件,若不存在,则创建文件
让fos与1.txt绑定
*/
}
}

•写字节数据的三个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class FileOutputStream_Demo02 {
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("1.txt");
fos.write(97);
fos.write(57);
fos.write(55);

byte[] bs1 ={100,101,102};
fos.write(bs1);

byte[] bytes = "HelloWorld".getBytes();
fos.write(bytes);
byte[] bs2 ={100,101,102};
fos.write(bs2,1,1);//数组,下标,个数
}
}

•如何追加续写
public FileOutputStream(String path,boolean append);//append表示是否追加
public FileOutputStream(File file,boolean append);//append表示是否追加

1
2
3
4
5
6
public class FileOutputStream_Demo03 {
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("1.txt",true);
fos.write(97);
}
}

•如何换行

1
2
3
4
5
6
7
8
9

public class FileOutputStream_Demo04 {
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("1.txt",true);
for (int i = 0; i < 10; i++) {
fos.write("java \n".getBytes());
}
}
}

•flush
public void flush();//对于字节输出流没用

•close
public void close();//关闭该流,释放资源
一个流使用完毕,及时释放流,别的程序就可以使用该资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class FileOutputStream_Demo04 {
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("1.txt", true);
for (int i = 0; i < 10; i++) {
fos.write("java \n".getBytes());
}

fos.close();

while (true) {
}
//程序不会停止,但是流已关闭,可以对文件进行增删改操作
}
}

4.4 字节输入流InputStream
顶层父类(抽象类)
共性方法:
public void close();//关闭该流,释放资源
public int read();//一次读一个字节
public int read(byte[] bs);//一次读一个字节数组,返回值表示实际读取的字节个数
public int read(byte[] bs,int startIndex,int len);//一次读一个字节数组的一部分(基本不用)

4.5 FileInputStream的作用
文件的字节输入流(向文件中读字节数据)
•构造方法
public FileInputputStream(String path);//必须传入文件的路径
public FileInputStream(File file);//必须传入文件的File对象

public FileInputStream(String path);//必须传入文件的路径

1
2
3
4
5
6
7
8
9
10
11
public class FileInputStream_Demo01 {
public static void main(String[] args) throws Exception {
FileInputStream fis = new FileInputStream("1.txt");
/*
创建文件对象
判断文件是否存在,如果存在,无动作
若不存在,则直接抛出异常FileNotFoundException
让fis与1.txt绑定
*/
}
}

•读取一个字节\

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class FileInputStream_Demo02 {
public static void main(String[] args) throws Exception {
FileInputStream fis = new FileInputStream("1.txt");
int read = fis.read();
System.out.println(read);//97
System.out.println((char)read);//a

//一次读取一个字节的标准循环代码
int b = 0;
while ((b = fis.read()) != -1) {
System.out.println(b);
System.out.println((char)b);
/*
(b = fis.read()) != -1
先读 fis.read()
赋值 b=读取字节
判断 b!=-1
*/

//释放资源
fis.close();
}
}
}

•读取一个字节数组

1
2
3
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 FileInputStream_Demo03 {
public static void main(String[] args) throws Exception {
FileInputStream fis = new FileInputStream("1.txt");
//读取字节数组
byte[] bs = new byte[20];//实际开发中,一般写1024byte即1KB,或其整数倍

// int len = fis.read(bs);
//
// System.out.println(len);
//
// System.out.println(Arrays.toString(bs));
// System.out.println(new String(bs,0,len));
int len =0;
while ((len = fis.read(bs))!=-1){
System.out.println(new String(bs,0,len));
}
/*
(len = fis.read(bs))!=-1
先读 fis.read(bs)
赋值 len = 实际读取读个数
判断 len !=-1
*/

//释放资源
fis.close();
}
}

4.6 字节流练习复制图片
•复制文件的过程
FileInputStream->一次读一个数组,写一个数组->FileOutputStream

•代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Copy_File {
public static void main(String[] args) throws Exception {
FileInputStream fis = new FileInputStream("1.txt");
FileOutputStream fos = new FileOutputStream("copy.txt");

byte[] bs = new byte[1024];
int len = 0;
while ((len = fis.read(bs)) != -1) {
//为了防止最后一次读取时,写入多余的数据
fos.write(bs,0,len);
}

//原则:先开后关
fos.close();
fis.close();
}
}

总结:
File创建方式:
public File(String path);
public File(String parent,String child);
public File(File parent,String child);

File常见方法:
public String getAbsolutePath();
public String getPath();
public String getName();
public long length();

public boolean exists();
public boolean isDirectory();
public boolean isFile();

public boolean mkdrir();
public boolean mkdrirs();
public boolean createNewFile();

public boolean delete();

public String[] list();
public File[] listFiles();

绝对路径和相对路径:
绝对路径:盘符开头
相对路径:当前目录的根目录

递归含义:
方法内部调用方法本身

递归为什么出现内存溢出:
方法不断入栈,栈满则溢出
IO流的分类和功能:
输入流,输出流,字节流(byte单位),字符流(char单位)

字节输出流FileOutputStream写出数据到文件:
public void write(int b);
public void write(byte[] bs);
public void write(byte[] bs,int startIndex,int len);

字节输入流FileInputStream读取文件数据:
public int read();
public int read(byte[] bs);

算法图解

1.2 二分查找
二分查找是一种算法,其输入是一个有序的元素列表(必须有序的原因稍后解释)。如果要 查找的元素包含在列表中,二分查找返回其位置;否则返回null。

一般而言,对于包含n个元素的列表,用二分查找最多需要log2n步,而简单查找最多需要n步。

可将一系列元素存储在一系列相邻的桶(bucket),即数组中。这些桶从0开始编号:第一个桶的位置为#0,第二个桶为#1,第三个桶为#2,以此类推。

1.3 大O表示法是一种特殊的表示法,指出了算法的速度有多快。

大O表示法指的并非以秒为单位的速度。大O表示法 让你能够比较操作数,它指出了算法运行时间的增速。

1.3.4 一些常见的大O运行时间 下面按从快到慢的顺序列出了你经常会遇到的5种大O运行时间。
 O(log n),也叫对数时间,这样的算法包括二分查找。
 O(n),也叫线性时间,这样的算法包括简单查找。
 O(n * log n),这样的算法包括第4章将介绍的快速排序——一种速度较快的排序算法。
 O(n2),这样的算法包括第2章将介绍的选择排序——一种速度较慢的排序算法。
 O(n!),这样的算法包括接下来将介绍的旅行商问题的解决方案——一种非常慢的算法。

我们获得的主要启示如下。
 算法的速度指的并非时间,而是操作数的增速。
 谈论算法的速度时,我们说的是随着输入的增加,其运行时间将以什么样的速度增加。
 算法的运行时间用大O表示法表示。
 O(log n)比O(n)快,当需要搜索的元素越多时,前者比后者快得越多。

1.4 小结
 二分查找的速度比简单查找快得多。
 O(log n)比O(n)快。需要搜索的元素越多,前者比后者就快得越多。  算法运行时间并不以秒为单位。
 算法运行时间是从其增速的角度度量的。
 算法运行时间用大O表示法表示。

需要同时读取所有元素时,链表的效率很高:读取第一个元素,根据其中的地址再读取第二个元素,以此类推。但如果你需要跳跃,链表的效率 真的很低。

需要随机地读取元素时,数组的效率很高,因为可迅速找到数组的任何元素。
在链表中,元素并非靠在一起的,你无法迅速计算出第五个元素的内存地址,必须先访问第一个元素以获取第二个元素的地址,再访问第二个元素以获取第三个元素 的地址,以此类推,直到访问第五个元素。

元素的位置称为索引。因此,不说“元素20的位置为1”,而说“元素20位于索引1处”。本书 将使用索引来表示位置。

需要在中间插入元素时,数组和链表哪个更好呢?使用链表时,插入元素很简单,只需修改 它前面的那个元素指向的地址。
而使用数组时,则必须将后面的元素都向后移。如果没有足够的空间,可能还得将整个数组复制到其他地方!因此,当需要在中间插入元素时,链表是更好的选择。

如果你要删除元素呢?链表也是更好的选择,因为只需修改前一个元素指向的地址即可。而使用数组时,删除元素后,必须将后面的元素都向前移。
不同于插入,删除元素总能成功。如果内存中没有足够的空间,插入操作可能失败,但在任何情况下都能够将元素删除。

需要指出的是,仅当能够立即访问要删除的元素时,删除操作的运行时间才为O(1)。通常我 们都记录了链表的第一个元素和最后一个元素,因此删除这些元素时运行时间为O(1)。

有两 种访问方式:随机访问和顺序访问。
顺序访问意味着从第一个元素开始逐个地读取元素。
链表只能顺序访问:要读取链表的第十个元素,得先读取前九个元素,并沿链接找到第十个元素。随机 10 访问意味着可直接跳到第十个元素。
本书经常说数组的读取速度更快,这是因为它们支持随机访问。
很多情况都要求能够随机访问,因此数组用得很多。

总结:数组查找快,增删改慢。链表查找慢,增删改快。

练习
1.1 假设你要为饭店创建一个接受顾客点菜单的应用程序。这个应用程序存储一系列点菜 单。服务员添加点菜单,而厨师取出点菜单并制作菜肴。这是一个点菜单队列:服务 员在队尾添加点菜单,厨师取出队列开头的点菜单并制作菜肴。
你使用数组还是链表来实现这个队列呢?(提示:链表擅长插入和删除,而数组擅长 随机访问。在这个应用程序中,你要执行的是哪些操作呢?)

数组,只需要查找。

1.2 我们来做一个思考实验。假设Facebook记录一系列用户名,每当有用户试图登录 Facebook时,都查找其用户名,如果找到就允许用户登录。由于经常有用户登录 Facebook,因此需要执行大量的用户名查找操作。假设Facebook使用二分查找算法, 而这种算法要求能够随机访问——立即获取中间的用户名。考虑到这一点,应使用数 组还是链表来存储用户名呢?

数组,查找快

1.3 经常有用户在Facebook注册。假设你已决定使用数组来存储用户名,在插入方面数组 有何缺点呢?具体地说,在数组中添加新用户将出现什么情况?

数组默认内存占用可能不足以插入新用户,最终创建新数组来插入新用户。

1.4 实际上,Facebook存储用户信息时使用的既不是数组也不是链表。假设Facebook使用 的是一种混合数据:链表数组。这个数组包含26个元素,每个元素都指向一个链表。
例如,该数组的第一个元素指向的链表包含所有以A打头的用户名,第二个元素指向的 链表包含所有以B打头的用户名,以此类推。
假设Adit B在Facebook注册,而你需要将其加入前述数据结构中。因此,你访问数组的 第一个元素,再访问该元素指向的链表,并将Adit B添加到这个链表末尾。现在假设你要查找Zakhir H。因此你访问第26个元素,再在它指向的链表(该链表包含所有以z打头的用户名)中查找Zakhir H。

请问,相比于数组和链表,这种混合数据结构的查找和插入速度更慢还是更快?你不必给出大O运行时间,只需指出这种新数据结构的查找和插入速度更快还是更慢。

是,快

2.3 选择排序

需要检查的元素数越来越少
随着排序的进行,每次需要检查的元素数在逐渐减少,最后一次需要检查的元素都只有一 个。既然如此,运行时间怎么还是O(n2)呢?这个问题问得好,这与大O表示法中的常数相关。 第4章将详细解释,这里只简单地说一说。
你说得没错,并非每次都需要检查n个元素。第一次需要检查n个元素,但随后检查的元素 数依次为n  1, n – 2, …, 2和1。平均每次检查的元素数为1/2 × n,因此运行时间为O(n × 1/2 × n)。 但大O表示法省略诸如1/2这样的常数(有关这方面的完整讨论,请参阅第4章),因此简单地写 作O(n × n)或O(n2)。

选择排序是一种灵巧的算法,但其速度不是很快。快速排序是一种更快的排序算法,其运行 时间为O(n log n)

2.4 小结
 计算机内存犹如一大堆抽屉。
 需要存储多个元素时,可使用数组或链表。
 数组的元素都在一起。
 链表的元素是分开的,其中每个元素都存储了下一个元素的地址。
 数组的读取速度很快。
 链表的插入和删除速度很快。
 在同一个数组中,所有元素的类型都必须相同(都为int、double等)。

3.1 递归
递归只是让解决方案更清晰,并没有性能上的优势。实际上,在有些情况下,使用循环的性能更好。

编写递归函数时,必须告诉它何时停止递归。正因为如此,每个递归函数都有两部分:基线条件(base case)和递归条件(recursive case)。递归条件指的是函数调用自己,而基线条件则指的是函数不再调用自己,从而避免形成无限循环。

一.线程池方式
1.1 线程池的思想
需要使用线程时,会临时创建一个线程,然后启动。而线程的创建,以及使用完毕后的销毁,都需要消耗性能的。
因此,
先创建好线程,在需要使用时直接使用,使用完毕后归还,等待下次继续使用,即线程池思想。

1.2 线程池的概念
线程池:其实就是一个容纳多个线程对象的容器,其中的线程可以反复使用,省去了频繁创建/销毁线程对象的操作,无需反复创建线程而消耗过多资源。

1.3 线程池的使用
线程池的顶层接口:
java.util.concuttrent.Executor

线程池的子接口:
java.util.concurrent.ExcutorService

线程池的工具类:其作用是帮助我们创建一个线程池对象(即ExcutorService的实现类对象)
java.util.concuttrent.Executors

工具类中的静态方法:创建一个线程池对象
public static ExecutorService newFixedThreadPool(int nThreads);//用于创建一个具有指定线程个数的线程池对象

如何向线程池中提交任务:
调用ExecutorService接口中规定的方法
public Future submit(Runnable task);//向线程池中提交无返回值的任务 public Future submit(Callable task);//向线程池中提交有返回值的任务,返回Future类型(封装线程执行完毕后结果的那个对象)

1.4 线程池的练习

1
2
3
4
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
public class ThreadPool {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//使用多态接收
ExecutorService service = Executors.newFixedThreadPool(3);


//向线程池中提交无返回值的任务
// for (int i = 0; i < 10; i++) {
service.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " Done");
}
});
// }

//向线程池中提交有返回值的任务
Future<Integer> future = service.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i < 101; i++) {
sum+=i;
}
return sum;
}
});
Integer result = future.get();//get方法具有阻塞功能,会等待任务执行完毕后再返回结果
System.out.println(result);

//如果想要整个进程停止,那么需要关闭线程池
service.shutdown();
}
}

二.死锁
2.1 什么是死锁
在多线程中,有多把锁,最后导致所有的线程都在等待中,造成的现象,称为死锁

2.2 产生死锁的条件
a.至少有2个线程
b.至少有2个锁对象
c.至少有synchronized嵌套

2.3 死锁演示

1
2
3
4
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
public static void main(String[] args) {

//2个锁对象
Object obj1 = new Object();
Object obj2 = new Object();

//2个线程
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
//必须有synchronized嵌套
synchronized (obj1) {
System.out.println("Thread 1 get obj1 and want obj2");
//方式一:sleep,不释放锁
// try {
// Thread.sleep(500);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
synchronized (obj2) {
System.out.println("Thread 1 get obj2,execute");
System.out.println(Thread.currentThread().getName() + "Done");
}
}
}
}
}).start();

new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronized (obj2) {
System.out.println("Thread 2 get obj2 and want obj1");
// try {
// Thread.sleep(500);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
synchronized (obj1) {
System.out.println("Thread 2 get obj1,execute");
System.out.println(Thread.currentThread().getName() + "Done");
}
}
}
}
}).start();
}

注意:如果出现了死锁,无解。只能尽量避免死锁

三.线程状态
3.1 线程的六种状态(A thread state. A thread can be in one of the following states:)
•新建状态-NEW
A thread that has not yet started is in this state.
刚创建,且未调用strat方法的状态

•运行状态-RUNNABLE
A thread executing in the Java virtual machine is in this state.
处于新建状态的线程start方法之后
可运行状态(就绪)
可运行状态(运行)
注意:只有新建状态的线程才能调用start()

•受(锁)阻塞状态-BLOCKED
A thread that is blocked waiting for a monitor lock is in this state.
状态进入:
线程运行过程中,遇到同步方法,线程需要锁对象,但是锁对象已被其他线程持有
状态返回:
其他线程释放锁,当前线程抢到锁,才能从锁阻塞回到可运行状态

•限时等待状态-TIMED_WAITING
A thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state.
状态进入:
线程指定到代码调用Thread.slee(毫秒值),线程就处于限时等待状态
状态返回:
sleep时间到了,返回RUNNABLE状态

•永久等待-WAITING
A thread that is waiting indefinitely for another thread to perform a particular action is in this state.
状态进入:
当前线程必须持有锁对象
调用锁对象的wait()方法
此时线程就会进入永久等待,进入之前,当前线程会自动释放锁对象
状态返回:
其他线程要持有锁(必须锁刚刚进入无限等待的线程释放的锁)
调用锁对象的notify()方法

注意:被唤醒的线程是没有锁对象的,会进入锁阻塞,直到其他线程释放锁对象,才能进入可运行状态

•TERMINATED
A thread that has exited is in this state.
当线程任务执行完毕,已退出的线程状态(消亡状态)
注意:处于消亡状态的线程,不能再调用start方法

thread

四.等待唤醒机制
4.1 等待唤醒机制(Wait和Notify)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class TestWating {
public static void main(String[] args) throws InterruptedException {

//creat lock
Object lock = new Object();

//create thread
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
//get in code block
System.out.println("Thread A get into Wating");
System.out.println("Thread A Get Lock");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//wake back to task
System.out.println("A Wake");
}
}
}).start();

Thread.sleep(1000);

//New Threak wake up Thread A
new Thread(new Runnable() {
@Override
public void run() {
//Same lock Object
synchronized (lock) {
System.out.println("Thread B Got Lock");
System.out.println("Thread B Wake A Up");
lock.notify();
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
}).start();
}
}

注意:
a.只有线程进入了线程等待,其他线程调用了notify()才有作用,否则也可以调用刚,但是没有任何作用
b.锁对象.notify方法只能唤醒一个锁对象,具体哪一个是随机的
c.锁对象.notifyAll方法可以唤醒多个线程,谁抢到锁谁执行

4.2 生产者与消费者
生产者与消费者是一个十分经典的多线程协作模式,弄懂生产者消费者问题能对多线程编程的理解更加深刻
所谓生产者消费者问题,实际上包含了两类线程:
•一类是生产者线程用于生产数据
•一类是消费者线程用于消费数据

为了解耦生产者和消费者的关系,通常会采用共享的数据区域,将像是一个仓库
•生产者生产数据之后直接放置在共享区域中,并不需要关系消费者行为
•消费者只需要从共享数据区其获取数据,并不需要关系生产者行为

生产者 —> 共享数据区 <— 消费者

(代码演示)
需求:
生产者消费案例
需要两个线程,线程1包子铺线程,赋值生产包子。线程2吃货线程,赋值吃包子。
如果没有包子,那么吃货线程等待,包子铺线程执行,执行完后唤醒吃货线程。
如果没有包子,那么包子铺线程等待,吃货线程执行,执行完后唤醒包子铺线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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
public class TestDemo {
public static void main(String[] args) throws InterruptedException {

ArrayList<String> arr = new ArrayList<String>();
arr.add("first");

Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (arr) {
while (true) {

if (arr.size() > 0) {
try {
System.out.println("Bread presence");
arr.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Add Bread");
arr.add("bread");
System.out.println("Bread Done");
arr.notify();
}}

}
});

Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {

synchronized (arr) {
if (arr.size() == 0) {
try {
System.out.println("No bread");
arr.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Ate bread");
arr.remove(0);
System.out.println("Call Boss");
arr.notify();
}}
}
});
t1.start();
t2.start();
}
}

五.定时器
5.1 什么是定时器
可以让某个线程在某个时间做指定的任务,或某个时间以后指定时间间隔中反复做某个任务

5.2 定时器Timer的使用
•构造方法
public Timer();//构造一个定时器

•成员方法
public void schedule(TimerTask task,long delay);//在指定时间之后,执行指定任务
public void schedule(TimerTask task,long delay,long period);//在指定时间之后,开始周期性执行任务,时间间隔period毫秒
public void schedule(TimerTask task,Date time);//在指定的时间点,执行指定的任务
public void schedule(TimerTask task,Date time,long period);//在指定时间第一次执行任务,之后周期性执行任务,间隔period毫秒

•案例演示

1
2
3
4
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
public class TestTimer {
public static void main(String[] args) {
Timer t1 = new Timer();
Timer t2 = new Timer();
Timer t3 = new Timer();
Timer t4 = new Timer();

t1.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("Timer 1 Done");
}
},1000);

t2.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("Timer 2 Done");
}
},1000,1000);

Calendar cc = Calendar.getInstance();
cc.add(Calendar.SECOND,10);
Date date = cc.getTime();


t3.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("Timer 3 Done");
}
}, date);

t4.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("Timer 4 Done");
}
},date,1000 );

t2.cancel();
t4.cancel();
}
}

总结:
1.线程池
a.怎么创建
ExecutorService service = Executos.newFixedThreadPool(int 线程个数);
b.提交任务
service.submit(Runnalbe task);//提交无返回任务
Future future = service.submit(Callable task);//提交有返回任务
通过future.get()该方法会阻塞,直到线程执行完毕,返回结果
c.关闭线程池
service.shutDown();

2.死锁
a.多个线程
b.多把锁
c.嵌套反方向获取锁

死锁只能避免,出现即无法解决

3.线程的状态
a.NEW(新建状态)
b.RUNNABLE(可运行状态)
c.TERMIINATED(消亡状态)
d.BLOCKED(阻塞状态)
e.TIMED_WATING(限时等待状态)
f.WATING(无限等待状态)*

怎么进入WATING
I.当前线程获取锁对象
II.调用锁对象.wait()方法
III.进入WATING之前自动释放锁对象
怎么唤醒
I.其他线程持有锁对象(必须刚刚释放的同一把锁)
II.调用锁对象的.notify()方法
III.WATING线程醒来,进入BLOCKED状态,直到再次获取对象状态

4.TImer
四个方法
public void schedule(TimerTask task,long delay);//在指定时间之后,执行指定任务
public void schedule(TimerTask task,long delay,long period);//在指定时间之后,开始周期性执行任务,时间间隔period毫秒
public void schedule(TimerTask task,Date time);//在指定的时间点,执行指定的任务
public void schedule(TimerTask task,Date time,long period);//在指定时间第一次执行任务,之后周期性执行任务,间隔period毫秒

一.synchronized关键字
1.1 AtomicInteger不足之处
回顾:可以保证对”变量”操作是原子性的(有序性,可见性)

无法解决:多行代码的原子性

1.2 多行代码的原子性安全问题——卖票案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

public class TicketTask implements Runnable {
int count = 100;
@Override
public void run() {
while (true) {
if (count > 0) {

try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Sold" + count);
count--;
} else {
break;
}
}
}
}


public class TestDemo {
public static void main(String[] args) {
TicketTask tt = new TicketTask();
Thread t1 = new Thread(tt);
Thread t2 = new Thread(tt);
Thread t3 = new Thread(tt);

t1.start();
t2.start();
t3.start();
//线程出现安全问题
//可能出现重复票据
//还可能出现0甚至-1这种非法数据
}
}

重复数据原因:当一个线程执行完卖票后,还没来得及对票数-1,就被其他线程抢走了CPU,导致其他线程也卖出了一样的票
非法数据原因:当剩下最后一张票时,多个线程都经过了if判断,进入卖票的代码块中,依次卖出1,0,-1张票

1.3 synchronized关键字介绍
a.synchronized是什么:synchronized是Java的关键字
b.synchronized作用:可以让多句代码具有原子性(当某个线程执行多句代码的过程中不被其他线程打断)

1.4 解决方法1:同步代码块
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
public class TicketTask implements Runnable {
int count = 100;
Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
if (count > 0) {
System.out.println("Sold" + count);
count--;
} else {
break;
}
}
}
}
}


public class TestDemo {
public static void main(String[] args) {
TicketTask tt = new TicketTask();
Thread t1 = new Thread(tt);
Thread t2 = new Thread(tt);
Thread t3 = new Thread(tt);

t1.start();
t2.start();
t3.start();
}
}

1.5 解决方法2:同步方法
格式:
public synchronized void 方法名(){

}

1
2
3
4
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

public class TicketTask implements Runnable {
int count = 100;
@Override
public void run() {
while (true) {
sellTicket();
}
}

public synchronized void sellTicket() {//默认当前任务对象
if (count > 0) {
System.out.println("Sold" + count);
count--;
}
}
}

public class TestDemo {
public static void main(String[] args) {
TicketTask tt = new TicketTask();
Thread t1 = new Thread(tt);
Thread t2 = new Thread(tt);
Thread t3 = new Thread(tt);

t1.start();
t2.start();
t3.start();
}
}

注意:
a.同步代码块和同步方法原理差不多,到底是同步代码块的同步锁需要自己指定,而同步方法的同步锁默认使用当前对象this
b.同步代码块可以是static的,如果同步方法是static的,默认使用当前类的字节码文件(.class文件)作为锁对象。

1.6 解决方法3:Lock锁
Lock是一个接口,我们需要使用其实现类ReentrantLock
ReentrantLock的API:
public ReentrantLock();
public void lock();//加锁
public void unlock();//解锁
格式:
ReentrantLock lock = new ReentrantLock();
lock.lock();//加锁
lock.unloc();//解锁

解决代码:

1
2
3
4
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
public class TicketTask implements Runnable {
int count = 100;
//创建一个lock锁
Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock();
if (count > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Sold" + count);
count--;
} else {
break;
}
lock.unlock();
}
}
}

public class TestDemo {
public static void main(String[] args) {
TicketTask tt = new TicketTask();
Thread t1 = new Thread(tt);
Thread t2 = new Thread(tt);
Thread t3 = new Thread(tt);

t1.start();
t2.start();
t3.start();
}
}

二.并发包
什么是并发包:这是JDK提供的,包含一个在高并发情况使用集合或工具,

2.1 CopyOnWriteArrayList
arraylist线程是不安全的(ArrayIndexOutOfBoundsException),需要CopyOnWriteArrayList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Mythread extends Thread{
// public static List<Integer> list = new ArrayList<>();//ArrayIndexOutOfBoundsException
public static List<Integer> list = new CopyOnWriteArrayList<>();

@Override
public void run() {
for (int i = 0; i < 10000; i++) {
list.add(i);
}
System.out.println("Done");
}
}

public class TestArrayList {
public static void main(String[] args) throws InterruptedException {
Mythread mt1 = new Mythread();
Mythread mt2 = new Mythread();
mt1.start();
mt2.start();

Thread.sleep(1000);
System.out.println(Mythread.list.size());
}
}

2.2 CopyOnWriteArraySet

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

public class TestHashSet {
public static void main(String[] args) throws InterruptedException {
MyThread mt1 = new MyThread();
mt1.start();

for (int i = 10000; i < 20000; i++) {
MyThread.set.add(i);
}

Thread.sleep(1000);
System.out.println(MyThread.set.size());
}
}

public class Mythread extends Thread {
// public static List<Integer> list = new ArrayList<>();
public static List<Integer> list = new CopyOnWriteArrayList<>();
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
list.add(i);
}
System.out.println("Done");
}
}

2.3 ConcurrentHashMap

1
2
3
4
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 MyThread extends Thread {
//public static HashMap<Integer,Integer> map = new HashMap<Integer, Integer>();
// public static Map<Integer, Integer> map = new Hashtable<Integer, Integer>();
public static ConcurrentHashMap<Integer, Integer> map = new ConcurrentHashMap<Integer, Integer>();
@Override
public void run() {
long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
map.put(i, i);
}
long end = System.currentTimeMillis();
System.out.println("Done "+(end-start));
}
}

public class TestHashMap {
public static void main(String[] args) throws InterruptedException {
// MyThread mt1 = new MyThread();
//
// mt1.start();
//
// for (int i = 10000; i < 20000; i++) {
// MyThread.map.put(i,i);
// }
for (int i = 0; i < 1000; i++) {
new MyThread().start();
}
Thread.sleep(1000*20);
System.out.println(MyThread.map.size());
}
}

小结:
HashMap线程不安全(多线程不能操作同一个HashMap)
Hashtable线程安全(多线程可以操作同一个HashMap),但是效率低
ConcurrentHashMap 线程安全的,并且效率较高

•为什么Hashtable效率低:Hashtable锁定整个哈希表,一个操作正在进行时
•为什么ConcurrentHashMap效率高:局部锁定(CAS),只锁定链表(桶)。对当前元素锁定时,它元素不锁定。

2.4 CountDownLatch
•CountDownLatch的作用:允许当前线程,等待其他线程完成某种操作之后,当前线程继续执行
•CountDownLatch的API
构造方法:
public CountDownLatch(int count);//初始化,需要传入计数器,需要等待的线程数
成员方法:
public void await() throws InterruptedException();//让当前线程等待
public void countDown()//计数器减1

•CountDownLatch使用案例
需求:
线程1需要执行打印:A和C,线程2要执行打印B
需要结果:线程1打印A,线程2打印B,之后线程A打印C,输出A B C

1
2
3
4
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 MyThread1 extends Thread {
private CountDownLatch latch;
public MyThread1(CountDownLatch latch) {
this.latch = latch;
}

@Override
public void run() {
System.out.println("A");
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public class MyThread2 extends Thread {
private CountDownLatch latch;
public MyThread2(CountDownLatch latch) {
this.latch = latch;
}

@Override
public void run() {
System.out.println("B");
latch.countDown();
System.out.println("D");
}
}

public class TestCountDownLatch {
public static void main(String[] args) {
//创建CountDownLatch
CountDownLatch latch = new CountDownLatch(1);
MyThread1 mt1 = new MyThread1(latch);
MyThread2 mt2 = new MyThread2(latch);

mt1.start();
mt2.start();
}
}

2.5 CyclicBarrier
•CyclicBarrier的作用:让多个线程都到达了某种要求之后,新的任务才能执行
•CyclicBarrier的API
构造方法:
public CycliBarrier(int parties,Runnable barrierAction);
//parties【需要多少线程】,Runnable barrierAction)【所有线程都满足要求了,执行任务】

成员方法:
public int await();//当所有线程都到达了,需要调用await

•CyclicBarrier使用案例

1
2
3
4
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 PersonThread extends Thread {
private CyclicBarrier cb1;
public PersonThread(CyclicBarrier cb1){
this.cb1 = cb1;
}

@Override
public void run() {
try {
Thread.sleep((int) (Math.random()) * 1000);
System.out.println(Thread.currentThread().getName() + "Done");
cb1.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}

}

public class TestPersonThread {
public static void main(String[] args) {
CyclicBarrier cb1 = new CyclicBarrier(5,new Runnable(){
@Override
public void run() {
System.out.println("Done");
}
});
PersonThread pt1 = new PersonThread(cb1);
PersonThread pt2 = new PersonThread(cb1);
PersonThread pt3 = new PersonThread(cb1);
PersonThread pt4 = new PersonThread(cb1);
PersonThread pt5 = new PersonThread(cb1);

pt1.start();
pt2.start();
pt3.start();
pt4.start();
pt5.start();
}
}

2.6 Semaphore
•Semaphore的作用:用于控制并发线程的数量

•Semaphore的API:
构造方法:
public Semaphore(int permits);//参数permit表示最多有多少个线程并发执行
成员方法:
public void acquire();//获取线程许可证
public void release();//释放线程许可证

•Semaphore使用案例

1
2
3
4
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
public class MyThread extends Thread{
private Semaphore semaphore;
public MyThread(Semaphore semaphore){
this.semaphore= semaphore;
}
@Override
public void run() {
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" "+System.currentTimeMillis());
try {
Thread.sleep(1000*new Random().nextInt(6));
} catch (InterruptedException e) {
e.printStackTrace();
}
// System.out.println(Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName()+" "+System.currentTimeMillis());
semaphore.release();
}
}

public class TestDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);

for (int i = 0; i < 10; i++) {
new MyThread(semaphore).start();
}
}
}

2.7 Exchanger
•Exchanger的作用:用于线程间的数据交换

•Exchanger的API:
构造方法:
public Exchanger();

成员方法:
public exchanger();//参数发送给别的线程的数据,返回值返回的数据

•Exchanger使用案例

1
2
3
4
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

public class MyThread1 extends Thread {
private Exchanger<String> exchanger;
public MyThread1(Exchanger<String> exchanger) {
this.exchanger = exchanger;
}

@Override
public void run() {
System.out.println("MyThread-1 send to Thread-2");

String result = null;

try {
result = exchanger.exchange("Get Thread-1");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread1 Get:"+result);
}
}

public class MyThread2 extends Thread {
private Exchanger<String > exchanger;
public MyThread2(Exchanger<String > exchanger){
this.exchanger=exchanger;
}

@Override
public void run() {
System.out.println("MyThread-2 send to Thread-1");

String result = null;

try {
result = exchanger.exchange("Get Thread-2");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread2 Get:"+result);
}
}

public class TestDemo {
public static void main(String[] args) {
Exchanger<String> exchanger = new Exchanger<String>();

MyThread1 mt1 = new MyThread1(exchanger);
MyThread2 mt2 = new MyThread2(exchanger);

mt1.start();
mt2.start();
}
}

总结:
使用同步代码块解决线程安全问题
synchronized(锁对象){
需要同步的代码(需要保证原子性的代码)
}
使用同步方法解决线程安全问题
public synchronized void 方法名(){
需要同步的代码(需要保证原子性的代码)
}
使用Lock锁解决线程安全问题
Lock lock = new ReentrantLock();
lock.lock();
需要同步的代码(需要保证原子性的代码)
lock.unlock();

说明volatile关键字和synchronize关键字区别
volatile能解决有序性和可见性
原子类 能解决变量的原子性(有序性和可见性)
synchronized 能解决多句代码的原子性(有序性和可见性)

描述CopyOnWriteArrayList类作用
代替多线程的情况,线程安全的ArrayList集合

描述CopyOnWriteArraySet类作用
代替多线程的情况,线程安全的HashSet集合

描述ConcurrentHashMap类作用
代替多线程的情况,线程安全的HashMap集合,比HashTable效率更高

描述CountDownLatch类作用
可以运行一个线程等待另一个线程执行完毕后再继续执行

描述CycliBarrrier类作用
让一组线程都到达某中调节后再执行某个任务

描述Semaphore类作用
控制多线程并发的最大数量

描述Exchanger类作用
用于线程间的通信(数据交换)

一.多线程
1.1 并行和并发
并行:两个事件,在同一时刻,都在发生
并发:两个事件,在同一时间段内,都在发生

1.2 进程和线程
进程:正在内存中运行的程序,称为进程
•是系统进行资源分配和调用的独立单位
•每一个进程都有自己的诶次空间和系统资源

线程:是进程中的单个顺序控制流,进程中完成某个小功能的模块(进程中用于执行某个功能的执行单元)
•单线程:一个进程如果只有一条执行路径,则称为单线程程序
•多线程:一个进程如果有多条执行路径,则称为多线程程序

•进程和线程的区别
线程是属于某个进程的
进程:每个进程都有自己独立的内存空间(栈/堆),并且至少有一个线程
线程:每个线程都会跟所在的进程申请一块独立的栈,堆共享进程的堆(堆是共享的,栈是独立的),线程小号的资源比进程小得多

•线程调度
线程调用是指CPU在不同的进程不同的线程之间进行快速切换

线程调度的分类:
分时调度:每个线程平均拥有CPU的执行权
抢占式调度:每个线程随机分配CPU的执行权(具体的分配多少和线程的优先级有关)
Java进程中,所有的线程,采用抢占式调度

1.3 Thread类的介绍
例:IDE的main方法运行时是创建一个进程,该进程中至少有2个线程
I.main方法所在的线程,称为主线程
II.垃圾回收期线程

a.Thread类是什么
Thread是Java定义好的,代表线程的类,只要创建该类的一个对象,其实介绍创建了一个线程

b.Thread类的构造方法
public Thread();//无参构造
public Thread(String name);//带有线程名的构造
public Thread(Runnable r);//带有线程任务的构造
public Thread(Runnable r,String name);//即带有线程名字,又有线程任务的构造

c.Thread类的成员方法
public String getName();//获取线程的名字。无参构造,线程会有自己的名字,如Thread0,Thread1
public String setName(String name);//修改线程的名字
public void run();//代表线程要执行的任务,任务有关的代码需要写在此方法中
public void start();//线程只创建并不会执行,必须调用start开启后才会执行任务

public static void sleep(long millis);//让当前线程休眠X毫秒(指Thread.sleep(1000)代码写在哪个线程中,所在的线程就当前线程)
public static Thread currentThread();//获取当前线程对象(指Thread.currentThread()代码写在哪个线程中,所在的线程就当前线程)

public static void join();//等待这个线程死亡
public static void setDaemon(boolean on);//将此线程标记为守护线程,当允许的线程都是守护线程时,Java虚拟机将退出

public final int getPriority();//返回此线程的优先级
public final void setPriority(int newPriority);//更改此线程的优先级
线程优先级高,仅表示获取CPU时间片的几率高,并不表示会优先执行

1.4 创建新的线程方式一:继承方式
a. API描述:将类声明为Thread的子类。该子类重写Thread类的run方法。接下来可以分配并启动该子类的实例
b.分析创建步骤:
I.创建子类 继承 Thread
II.子类中重写run方法(在run中编写线程要执行的任务代码)

•为什么要重写run()方法:因为run()是用来封装被线程执行的代码
•run()方法和start()方法的区别:
i.run()封装线程执行的代码,直接调用,相当于普通方法的调用
ii.start()启动线程,然后由JVM调用此线程的run()方法

III.创建子类对象(实际上就是创建一个线程对象)
IV.调用线程对象的start方法(启动该线程)
案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyThread extends Thread{
@Override
public void run(){
for (int i = 0; i < 50; i++) {
System.out.println(getName()+i);
}
}
}

public class ThreadDemo {
public static void main(String[] args) {
//创建子类对象(实际上就是创建一个线程对象)
MyThread mt = new MyThread();
//调用线程对象的start方法(启动该线程)
mt.start();
//主线程 不会等待子线程任务结束
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
}

}
总结:
a.可以给线程起名,也可以使用默认名
b.获取线程名称时,建议使用通用方式Thread.currentThread().getName();子线程内部,也可以直接调用getName();

1.5 创建新的线程方式二:实现方式
a.API描述:声明实现Runnable接口的类。该类然后实现run方法。然后可以分配该类的实例,在创建Thread时作为一个参数来传递并启动

b.分析创建步骤:
I.创建实现类 实现Runnable接口(实际上接口中就一个任务方法:run方法)
II.实现类重写run方法(run中编写具体的任务代码)
III.创建实现类对象(该实现类对象并不少线程对象,称为任务对象)
IV.创建Thread对象,同时传入实现类对象public Thread(Runnable r);
V.启动该线程(调用线程对象的start方法)

c.代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//创建实现类,实现Runnable接口
public class MyRunnable implements Runnable {
//实现类重写run方法(run中编写具体的任务代码)
@Override
public void run() {
//run方法中写任务代码
for (int i = 0; i < 50; i++) {
System.out.println(i);
}
}
}


public class TestThread {
public static void main(String[] args) {
//创建实现类对象(该实现类对象并不少线程对象,称为任务对象)
MyRunnable mr = new MyRunnable();
//创建Thread对象,同时传入实现类对象public Thread(Runnable r);
Thread tt = new Thread(mr);
//启动该线程(调用线程对象的start方法)
tt.start();
//主线程不会等待子线程执行完毕
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
}
}

1.5.1 两种线程的优劣势比较
两种创建线程的方式,实现方式比较好
I.实现方式的线程和任务是分开的,由程序员自己组合的,适合多个相同的程序代码的线程去共享同一个资源。
II.实现方式避免了java中的单继承的局限性。
III.实现方式线程和任务是接耦的,继承方式线程和任务是紧耦合的。增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
IV.线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的子类。
综上所述,在开发中建议使用实现方式(并不是说实现方式不对)

1.6 匿名内部类简化线程创建方式
匿名内部类的作用:可以快速创建一个类的子类对象,或者一个接口的实现类对象

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

public class TestDemo {
public static void main(String[] args) {
new Thread() {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
}
}.start();
new Thread (new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
}
}).start();

for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
}

}
二.高并发和线程安全
2.1 高并发及线程安全介绍
什么是高并发:在某个时间点上,大量的用户(线程),访问同一个资源。
线程安全:在某个时间点上,大量用户(线程)访问同一个资源时,由于线程运行机制的原因,可能导致被访问的资源出现类”数据污染”

2.2 多线程的运行机制[内存方面]

1
2
3
4
5
6
7
8
9
 public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
} }

public class Demo {
public static void main(String[] args) {
} }

2.3 多线程的安全性问题:可见性
当一个共享的变量被多个线程使用时,当其中某个线程对其进行了修改,该值对其他线程并非立即可见的,其他线程获取的值,还是以前的副本(旧值)。

1
2
3
4
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
public class MyThread extends Thread {
public static int a = 0;
@Override
public void run() {
System.out.println("线程启动,休息2秒...");
try {
Thread.sleep(1000 * 2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("将a的值改为1");
a = 1;
System.out.println("线程结束...");
}
}


public class TestSafeDemo01 {
public static void main(String[] args) {
//1.启动线程
MyThread t = new MyThread();
t.start();
//2.主线程继续
while (true) {
if (MyThread.a == 1) {
System.out.println("主线程读到了a = 1");
}
}
}
}

2.4 多线程的安全性问题:有序性
单线程会在不影响代码的结果的程度上,对代码进行重排。
如果在在”多线程”情况下,可能对一样的代码执行后得出不一样的结果。
我们要保证在多线程的情况下,不对代码进行”重排”,保证代码是有序的(不要使用重排)

2.5 多线程的安全性问题:原子性
什么是原子性:线程对一个共享变量进行++时,这个++是分成两部操作的,先取出值加1,然后给共享变量赋值,如果取出值加1后,还没有来得及赋值就被其他线程抢走CPU,此时我们称++操作不具有原子性

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 MyThread extends Thread {
public static int a = 0;
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
a++;
}
System.out.println("Done");
}
}

public class TestSaftDemo {
public static void main(String[] args) throws InterruptedException {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start();//Thread 1对a加了一万次
t2.start();//Thread 2对a加了一万次

Thread.sleep(1000);
System.out.println(MyThread.a);
}
}

三.volatile关键字
3.1 volatile是什么
volatile是一个关键字,用来修饰成员变量(静态变量),被他修改的变量,具有可见性和有序性

3.2 volatile解决可见性

1
2
3
4
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 MyThread extends Thread {
public volatile static int a = 0;
@Override
public void run() {
System.out.println("线程启动,休息2秒...");
try {
Thread.sleep(1000 * 2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("将a的值改为1");
a = 1;
System.out.println("线程结束...");
}
}

public class TestSafeDemo01 {
public static void main(String[] args) {
//1.启动线程
MyThread t = new MyThread();
t.start();
//2.主线程继续
while (true) {
if (MyThread.a == 1) {
System.out.println("主线程读到了a = 1");
}
}
}
}

3.3 volatile解决有序性

3.4 volatile不能解决原子性
即使加了volatile,仍然会被其他线程抢占,无法解决原子性问题

小结:volatile作用
a.解决变量的可见性,一旦变量发生改变,所有使用到该变量的线程都会取到最新值
b.解决变量的有序性,一旦变量加上volatile,那么编译器不会对变量运行的代码进行重排
c.无法解决变量操作过程中的原子性,对变量的操作哈斯可能被其他线程打断

四.原子类
4.1 原子类概述
a.什么是原子类:是对普通类型(如:int,Integer,double ,Double)的原子类封装,使其的操作成员原子操作
b.原子类的作用:对原子类的增强或减少操作,保证其原子性,保证中间不会被其他线程”打断”
c.原子类有哪些:

在java.util.concurrent.atomic包下定义了一些对“变量”操作的“原子类”:
1).java.util.concurrent.atomic.AtomicInteger:对int变量操作的“原子类”;
2).java.util.concurrent.atomic.AtomicLong:对long变量操作的“原子类”;
3).java.util.concurrent.atomic.AtomicBoolean:对boolean变量操作的“原子类”;
它们可以保证对“变量”操作的:原子性、有序性、可见性。

4.2 AtomicInteger类示例
a.AtomicInteger是什么:是对int类型变量进程操作原子类
b.AtomicInteger的构造方法:public AtomicInteger(int num);
c.AtomicInteger的成员方法:
public int getAndIncrement();//就相当于我们的 变量++
public int ncrementAndGet();//就相当于我们的 ++变量

d.AtomicInteger的改写案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MyThread extends Thread {
public static int a = 0;

@Override
public void run() {
for (int i = 0; i < 10000; i++) {
a++;
}
System.out.println("Done");
}
}


public class TestSaftDemo {
public static void main(String[] args) throws InterruptedException {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start();//Thread 1对a加了一万次
t2.start();//Thread 2对a加了一万次

Thread.sleep(1000);
System.out.println(MyThread.a);
}
}

4.3 AtomicInteger类底层的工作原理-CAS机制
CAS:Compare And Swap(自旋)
我们假设两个线程交替运行的情况,看看它是怎样工作的:
初始AtomicInteger的值为0
线程A执行:var5 = this.getIntVolatile(var1,var2);获取的结果为:0
线程A被暂停
线程B执行:var5 = this.getIntVolatile(var1,var2);获取的结果为:0
线程B执行:this.compareAndSwapInt(var1,var2,var5,var5 + var4)
线程B成功将AtomicInteger中的值改为1
线程A恢复运行,执行:this.compareAndSwapInt(var1,var2,var5,var5 + var4)
此时线程A使用var1和var2从AtomicInteger中获取的值为:1,而传入的var5为0,比较失败,返回 false,继续循环。
线程A执行:var5 = this.getIntVolatile(var1,var2);获取的结果为:1
线程A执行:this.compareAndSwapInt(var1,var2,var5,var5 + var4)
此时线程A使用var1和var2从AtomicInteger中获取的值为:1,而传入的var5为1,比较成功,将其修改 为var5 + var4,也就是2,将AtomicInteger中的值改为2,结束。

•CAS也被称为乐观锁:因为大部分比较的结果为true,就直接修改了。只有少部分多线程并发的情况会 导致CAS失败,而再次循环。

4.4 AtomicIntegerArray类示例
•非原子类数组在多线程并发时会有问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MyThread extends Thread {
public static int[] intArray = new int[1000];
@Override
public void run(){
for (int i = 0; i < intArray.length; i++) {
intArray[i]++;
}
}
}

public class TestDemo {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new MyThread().start();
}
Thread.sleep(1000*5);
System.out.println("sleep 5 second");
for (int i = 0; i <MyThread.intArray.length ; i++) {
System.out.println(MyThread.intArray[i]);
}
}
}

有极个别元素小于1000,因为int[]不是原子类数组,

•使用原子类数组,保证原子性,解决问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MyThread extends Thread {
public static int[] intArray = new int[1000];
public static AtomicIntegerArray arr = new AtomicIntegerArray(intArray);

@Override
public void run(){
for (int i = 0; i < arr.length(); i++) {
arr.addAndGet(i, 1);
}
}
}

public class TestDemo {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new MyThread().start();
}
Thread.sleep(1000*5);
System.out.println("sleep 5 second");
for (int i = 0; i <MyThread.arr.length() ; i++) {
System.out.println(MyThread.arr.get(i));
}
}
}

总结:
进程和线程的区别:
进程:正在内存中运行的程序
线程:进程中完成某个功能的执行单元

并发于并行的区别:
并行:两个线程真的一起运行
并发:两个线程看起来一起运行,实际交替执行

Java多线程原理[CPU]:
线程调度

继承子类方式创建多线程:
I.子类 继承 Thread
II.子类 重写 run
III.创建 子类 对象
IV.调用 子类 对象 start

接口实现类方式实现多线程:
I.实现类 实现 Runnable
II.实现类 重写 run
III.创建 实现类 对象
IV.创建 Thread对象 同时 传入 实现类 对象
V.调用 Thread 对象 start 方法
VI.使用匿名内部类快速创建继承方式和实现方式的线程

接口实现方式好处:
I.实现方式的线程和任务是分开的,由程序员自己组合的,适合多个相同的程序代码的线程去共享同一个资源。
II.实现方式避免了java中的单继承的局限性。
III.实现方式线程和任务是接耦的,继承方式线程和任务是紧耦合的。增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
IV.线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的子类。

安全问题出现的原因:
可见性 有序性 原子性

volatile关键字作用:
解决可见性与有序性,不能解决原子性

原子类AtomicInteger使用:
解决原子性
创建:
AtomicInteger i = new AtomicInteger(1);
使用:
i.getAndIncrement();//i++
i.incrementAndGet();//++i

原子类工作机制:
原子类底层使用CAS机制

一.选择排序
1.1 选择排序概述
核心思想:选中第一个元素,取出以后的元素依次和选中的元素进行比较,大的往后走,小的往前走,接着选中第二个元素同样操作,依此类推

1.2 选择排序图解

1.3 选择排序代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
int[] arr = {5, 4, 7, 1, 8, 2, 3, 6, 9};
//外层循环,控制选中的元素
for (int i = 0; i < arr.length - 1; i++) {
//内存循环,控制元素的比较
for (int j = i + 1; j < arr.length; j++) {
//比较arr[i] arr[j]
if (arr[i] > arr[j]) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
System.out.println(Arrays.toString(arr));
}

二.二分查找
2.1 普通查找和二分查找
普通查找:给定数组,从数组中找到某个元素的索引
int[] arr = {4,5,6,1,7,2,8,9};
找出7出现的索引,只能从前往后找。
二分查找:给定的数组,必须是有自然顺序的,从数组中找某个元素出现的索引
int[] arr = {1,3,5,6,8,9,10,12};
找出8出现的索引,我们依然可以从前到后遍历,但是效率低。从中间开始,根据中间值的大小,可以让查找范围缩小一般

2.2 二分查找图解

a.首先定义两个遍历
int left = 0;
int right = arr.length-1

循环{
b.获取中间索引
int middle = (left+right/2)
c.获取middle索引元素和目标值比较
if (arr[middle] > key) {
right = middle = 1;
} else if (arr[middle] < key) {
left = middle + 1;
} else {
return middle;
}
}

2.3 二分查找代码实现

1
2
3
4
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

public static void main(String[] args) {
int[] arr = {10, 20, 30, 40, 50, 60, 70, 88, 90};
//查找元素
int key = 20;
//调用方法
int index = Binary_Search(arr, key);
System.out.println(index);

int key_1 = 200;
int index_1 = Binary_Search(arr, key_1);
System.out.println(index_1);
}

//二分查找
public static int Binary_Search(int[] arr, int key) {
//开始和结束索引
int min = 0;
int max = arr.length - 1;
while (min <= max) {
//获取中间索引
int middle = (min + max) / 2;
//比较中间索引的元素和key
if (arr[middle] > key) {
max = middle - 1;
} else if (arr[middle] < key) {
min = middle + 1;
} else {
return middle;
}
}
return -1;
}

三.异常
3.1 什么是异常
异常:写程序或者运行程序过程中遇到的非正常返回

3.2 异常的继承关系
所有异常和错误的根类:Throwable
|-Error 错误类
|-Exception 异常类
|–RuntimeException
|–非RuntimeException

Error:严重问题,不需要处理
Exception:称为异常类,它表示程序本身可以处理的问题
•RuntimeException:在编译期不检查,出问题后,需要修改代码解决
•非RuntimeException:编译期将必须处理,否则程序不能通过编译,更不能正常运行

3.3 异常中常用的三个方法-Throwable的成员方法

方法名 说明
public void printStackTrace(); 以深红色在控制台打印异常的详细信息(包括异常类型,异常的原因,异常的位置)【最常用】
public String getMessage(); 返回此throwable的详细消息字符串
public String toString(); 获取异常的类型和异常的描述信息
打印方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
try{}
int[] arr ={1,2,3};
System.out.println(arr[3]);
}catch(ArrayIndexOutOfBoundsException e){
System.out.println(e.getMessage());
System.out.println(e.toString());
e.printStackTrace();
// Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3
// at com.test.Demo03_Exception01.TestException.main(TestException.java:6)
}
}

3.4 异常的分类
•编译时的异常
写好代码之后,运行代码之前出现的异常
编译时异常:Exception以及Exception的子类(RuntimeException除外)

•运行时的异常
运行代码时出现的异常
运行时异常:RuntimeException以及其子类

编译时异常和运行时异常的区别:
Java中的异常被分为两大类:编译时异常和运行时异常,也被称为受检异常和非受检异常
所有的RuntimeException类及其子类被称为运行时异常,其他的异常都是编译时异常

3.5 JVM异常产生过程 - 默认处理方案
如果程序出现问题,且未做任何处理,最终JVM会做出默认的处理

把异常名称,异常原因以及异常出现的位置等信息输出在控制台
程序停止执行
四.异常处理
Java中异常相关的五个关键字
throw
throws
try…cache
finally

4.1 抛出异常throw
a.throw是什么:throw是一个关键字
b.什么时候使用到throw:想要向上抛出异常时,使用throw关键字
c.使用格式:throw 异常对象
d.案例演示

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

public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5, 6};
int element = getElement(arr);
System.out.println(element + "\n");
int[] new_arr = {1, 2, 3};
int new_element = getElement(new_arr);
System.out.println(new_element);
/*
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Out of Array
at com.test.Demo03_Exception01.TestException02.getElement(TestException02.java:18)
at com.test.Demo03_Exception01.TestException02.main(TestException02.java:10)
*/
}

public static int getElement(int[] arr) {
//自己判断数组是否有3索引
if (arr.length < 4) {
throw new ArrayIndexOutOfBoundsException("Out of Array");
}
//获取数组中索引为3的元素
int number = arr[3];
return number;
}

4.2 Objects中非空判断方法
public static T requireNonNull(T obj);方法内部帮助我们判断是否为null

查看源码:

1
2
3
4
5
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException;
return obj;
}

4.3 遇到异常的两种处理方式
如果遇到编译时异常,可以使用以下两种方式处理
如果我们遇到运行时异常,编译时期不用处理,运行后根据异常信息修改代码即可

4.4.1 throws声明抛出异常
a.声明异常的格式
throws关键字是给方法使用的,为该方法做出声明,声明该方法内部有编译时异常,调用者需要使用该异常格式:

public void 方法名(参数列表)throws XxxException{
代码(如果代码有异常)
}

b.案例演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) throws FileNotFoundException{
ReadFiles("1.txt");

}

//throws关键字是给方法用的,为该方法做出声明,声明该方法内部有编译时异常,调用者需要处理该异常
public static void ReadFiles(String name) throws FileNotFoundException {
//假设硬盘上有一个1.txt文件
if (("1.txt").equals(name)) {
System.out.println("Successful");
} else {
//抛出异常
throw new FileNotFoundException("No File:" + name);
}
}

4.4.2 try-cache捕获异常
a.捕获异常的格式
格式:
try {
可能出现异常的代码
} catch (XxxException e) {
//处理异常
e.printStackTrace();//直接打印(开发阶段)
save(e);//将异常保存到异常日志
}

执行流程:
程序从try里面的代码开始执行
出现异常,会自动生成一个异常类对象,该异常对象被提交给Java Runtime系统
当Java Runtime系统接收到异常对象时,会到catch中去找匹配的异常类,找到后进行异常的处理
执行完毕后,程序还可以继续往下执行

b.案例演示

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

try {
ReadFiles("1.txt");
}catch (Exception e){
System.out.println("Has Exception");
e.printStackTrace();
}
System.out.println();

try {
ReadFiles("2.txt");
}catch (Exception e){
System.out.println("Has Exception");
e.printStackTrace();
}
System.out.println("Continue");
}
//throws关键字是给方法用的,为该方法做出声明,声明该方法内部有编译时异常,调用者需要处理该异常
public static void ReadFiles(String name) throws FileNotFoundException {
//假设硬盘上有一个1.txt文件
if (("1.txt").equals(name)) {
System.out.println("Successful");
} else {
//抛出异常
throw new FileNotFoundException("No File:" + name);
}
}

c.捕获到异常之后,如何查看异常的信息
I.直接带你出来,调用e.printStackTrace();//一般开发阶段
II.可以先保存起来,比如保存到异常日志。e.save();//用户使用阶段

4.5 finally代码块
a.finally代码块的格式
finally一般不能单独使用,配合try…catch使用
try {
可能出现异常的代码
} catch (XxxException e) {
//处理异常
e.printStackTrace();//直接打印(开发阶段)
save(e);//将异常保存到异常日志
}finally{
写在finally中的代码,不论是否有异常,都会执行
}
b.finally代码块的作用
写在finally中的代码,不论是否有异常,都会执行
一般用于写释放资源,关闭连接等代码

c.案例演示

1
2
3
4
5
6
7
8
try {
ReadFiles("2.txt");
}catch (Exception e){
System.out.println("Has Exception");
e.printStackTrace();
}finally {
xxx.close();
}

7.异常的注意事项
•运行时异常被抛出可以不处理,不需要throws声明,也不需要try…catch捕获
•如果父类的方法抛出了多个异常,子类覆盖(重写)父类方法时,只能抛出相同的异常或者它的子集(即少于父类方法)
•父类方法没有抛出异常,子类在重写该方法时,必须也不能抛出异常
•当多异常分别处理时,捕获处理,前面的类不能是后面类的父类
I.每个异常单独try…catch(一般不使用)
try{
method1();
}catch(1 e1){
}
try{
method2();
}catch(2 e2){

}
II.所有异常一起try,但是分开catch(偶尔使用)
try{
method1();
method2();
}catch(1 e1){

}catch(2 e2){

}
注意事项:要求前面的异常必须是子类异常,后面的异常必须是父类异常

III.所有异常一起try,一个catch【经常使用】
try{
method1();
method2();
method3();
}catch(Exception e1){

}

•在try/catch后可以追加finally代码块,其中的代码一定会被执行,通常用于资源回收。

五.自定义异常
5.1 为什么要定义异常
a.什么叫做自定义异常:自定义一个异常类,然后在适当时创建异常对象,并抛出
b.为什么要自定义异常:JDK提供的异常类不可能描述开发中所有问题

5.2 自定义异常的步骤
a.形似:创建一个类,类名必须叫XxxException
b.神似:继承Exception或RuntimeException
c.一般来说需要提供两个构造,无参构造+带有String参数的构造

1
2
3
4
5
6
7
8
9
10
public class MyException extends RuntimeException {
//无参构造
public MyException() {}

//带有异常信息的构造
public MyException(String message) {
//保存message参数
super(message);
}
}

5.3 自定义异常的练习
模拟操作,若用户名已存在,则抛出异常
a.JDK不带该异常,所以要自动抛出
b.

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

public class TestDemo {
public static void main(String[] args) {
String name = new Scanner(System.in).nextLine();
try {
register(name);
} catch (RegisterException e) {
e.printStackTrace();
}
}
public static void register(String name) throws RegisterException {
ArrayList<String> usernames = new ArrayList<>();
Collections.addAll(usernames, "a", "b", "c");

if (usernames.contains(name)) {
throw new RegisterException("UserName Repeat");
} else {
System.out.println("Regeist Successful");
}
}
}

总结:
a.选择排序的执行过程
for (int i = 0; i < arr.length - 1; i++) {
for (int j = i + 1; j < arr.length; j++) {
if (arr[i] > arr[j]) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
b.二分查找
public static int Binary_Search(int[] arr, int key) {
int min = 0;
int max = arr.length - 1;
while (min <= max) {
int middle = (min + max) / 2;
if (arr[middle] > key) {
max = middle - 1;
} else if (arr[middle] < key) {
min = middle + 1;
} else {
return middle;
}
}
return -1;
}
c.能分辨程序中的异常和错误的区别
所有异常和错误的根类:Throwable
|-Error 错误类 一般是硬件引起,程序员一般无法处理
|-Exception 异常类,异常一般是由程序编写不当造成的,程序员有能力处理
|–编译时异常 Exception以及其子类(RuntimeException除外)
|–运行时异常 RuntimeException以及其子类
d.三个常见的运行时异常
ArrayIndexOutOfBoundsException
NullPointerException
ClassPathException向下转型时出现的类型转换异常
e.使用try…catch关键字处理异常
try{

}catch()XxxException e{
e.printStackTrace();
}
f.使用throws关键字处理异常
throws给方法用的,表名抛出异常,该调用的调用者要处理异常
public void 方法名(参数列表) throws Exception(){}
g.自定义异常

一.Map集合
1.1 Map的概述以及特点
什么是Map集合:
Collection集合称为单列集合,Map集合称为双列集合,key-value,键-值,名称:Entry
Map集合的特点:
a.Collection每个元素单独存在(单列),Map每个元素成对存在(双列)
b.Map集合中,键必须是唯一的,值是可以重复的
c.Collection中,泛型只有一个。Map<K,V>中,泛型有两个(其中K代表键Key的类,V代表值Value类)

1.2 Map的3个常用实现类以及其特点
Map接口有三个常见的实现类:
HashMap<K,V>:
底层采用哈希表结构
无序

LinkedHashMap<K,V>:
底层采用链表+哈希表结构
有序

TreeMap<K,V>:
底层采用红黑树结构
无序,键有自然顺序的,即按照键的自然顺序输出

重点:Map中为了保证键的唯一性,如果键是自定义类型,必须重写键的hashCode和equals方法

1.3 Map接口中定义的通用方法
增:public V put(K key, V value) ;添加一个键值对Entry,返回null
删:public V remove(Object key);根据键Key去删键值对Entry,返回被删除的键值对的值
查:public V get(Object key);根据键Key获取对应的值Value
改:public V put(K key, V value) ;添加一个重复的键时,该方法变为修改,返回修改前的值
其他:
public boolean containKey(Object K);判断Map中是否包含该键
public boolean containKey(Object V);判断Map中是否包含该值

1
2
3
4
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 static void main(String[] args) {
HashMap<String ,Integer> map = new HashMap<String,Integer>();
//增加
map.put("a",1);
map.put("b",2);
map.put("c",3);
map.put("d",4);
map.put("e",5);
map.put("f",6);
System.out.println(map);
//删除
Integer v1 = map.remove("a");
System.out.println(v1);
System.out.println(map);
//删除不存在的v
Integer v2 = map.remove("g");
System.out.println(v2);
System.out.println(map);
//获取
Integer v3 = map.get("a");
System.out.println(v3);
System.out.println(map);
//修改也是调用put
Integer v4 = map.put("c", 12);
System.out.println(v4);
System.out.println(map);
//判断
boolean k = map.containsKey("a");
System.out.println(k);
boolean v = map.containsValue(5);
System.out.println(v);
}

1.4 Map的遍历
•遍历方式一:以键找值
public Set keySet() : 获取Map集合中所有的键,存储到Set集合中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
HashMap<String, Integer> map = new HashMap<String, Integer>();
//增加
map.put("a", 1);
map.put("b", 2);
map.put("c", 3);
map.put("d", 4);
map.put("e", 5);
map.put("f", 6);
Set<String> keys = map.keySet();
for (String key : keys) {
Integer value = map.get(key);
System.out.println(key);
System.out.println(value);
}
}

•遍历方式二:键值对方式
public Set<Map.Entry<K,V>> entrySet() : 获取到Map集合中所有的键值对对象的集合(Set集合)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
HashMap<String, Integer> map = new HashMap<String, Integer>();
//增加
map.put("a", 1);
map.put("b", 2);
map.put("c", 3);
map.put("d", 4);
map.put("e", 5);
map.put("f", 6);
//Map集合遍历的第二种方式:键值对方式
//获取所有键值对
Set<Map.Entry<String, Integer>> entries = map.entrySet();
//遍历entrys集合
for (Map.Entry<String, Integer> entry : entries) {
//从entry种取出键值
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key+"..."+value);
}
}

1.5 HashMap存储自定义类型的键
需求:创建一个Map,学生为键,家庭地址作值。

学生键:

1
2
3
4
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
public class Student {
String Name;
int age;
public Student(){

}

@Override
public String toString() {
return "Student{" +
"Name='" + Name + '\'' +
", age=" + age +
'}';
}

public Student(String name, int age) {
Name = name;
this.age = age;
}
//为了保证学生键的唯一性,要重写hashCode和equals
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Student student = (Student) o;
return age == student.age &&
Objects.equals(Name, student.Name);
}

@Override
public int hashCode() {
return Objects.hash(Name, age);
}
}

测试Demo:

1
2
3
4
5
6
7
8
9
10
11

public static void main(String[] args) {
HashMap<Student,String > std= new HashMap<Student,String>();
std.put(new Student("a",1), "PK");
std.put(new Student("b",2), "GZ");
std.put(new Student("c",3), "SH");
std.put(new Student("d",4), "SZ");
System.out.println(std);
std.put(new Student("b",2), "SC");
System.out.println(std);
}

结论:如果键是自定义类型,那么为了保证键的唯一性,必须重写hashCode和equals方法

1.6 LinkedHashMap介绍
HashMap底层采用哈希表结构,所以无序
LinkedHashMap底层采用的是链表结构+哈希表,所以是有序的

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
LinkedHashMap<String ,Integer> LHS = new LinkedHashMap<String ,Integer>();
LHS.put("a", 1);
LHS.put("c", 1);
LHS.put("d", 1);
LHS.put("b", 1);

System.out.println(LHS);
}

1.7 TreeMap集合
TreeMap底层采用的是红黑树结构,无序的,但是会以键的自然顺序输出

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
TreeMap<String ,Integer> TM = new TreeMap<String,Integer>();
TM.put("a", 1);
TM.put("c", 2);
TM.put("e", 3);
TM.put("d", 4);
TM.put("b", 5);

System.out.println(TM);
}

扩展:
如果键是数值类型,那么按照数值的大小升序
如果键是Character类型,那么按照字母的码值排序
如果键是String类型,那么按照字母首字母排序,若一样,次字母排序,依次类推
这四种结论是一样的:Arrays.sort() Collections.sort() TreeSet TreeMap

我们也可以使用比较器排序

1
2
3
4
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

public static void main(String[] args) {
TreeMap<String, Integer> TM = new TreeMap<String, Integer>();
TM.put("a", 1);
TM.put("c", 2);
TM.put("e", 3);
TM.put("d", 4);
TM.put("b", 5);

System.out.println(TM);

TreeMap<Integer, String> new_TM = new TreeMap<Integer, String>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
new_TM.put(1, "a");
new_TM.put(3, "a");
new_TM.put(2, "a");
new_TM.put(5, "a");
new_TM.put(4, "a");

System.out.println(new_TM);
TreeMap<Student, String> std = new TreeMap<Student, String>(new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o2.Name.length() - o1.Name.length();
}
});

std.put(new Student("aa", 1), "PK");
std.put(new Student("bbb", 2), "GZ");
std.put(new Student("cc", 3), "SH");
std.put(new Student("ddddd", 4), "SZ");

System.out.println(std);
}

1.8 Map集合练习
输入一个字符串,统计字符串中每个字符出现的次数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
//定义一个Map
LinkedHashMap<Character, Integer> LHM = new LinkedHashMap<Character, Integer>();
//输入一个字符串
System.out.println("input char");
String str = new Scanner(System.in).nextLine();
//遍历字符串
for (int i = 0; i < str.length(); i++) {
// 取出字符串中的某个字符
char ch = str.charAt(i);
//这个字符ch以前出现过
if (LHM.containsKey(ch)) {
Integer oldCount = LHM.get(ch);
LHM.put(ch, oldCount + 1);
} else {
LHM.put(ch, 1);
}
}
System.out.println(LHM);
}

二.集合的嵌套
什么是集合嵌套
集合中的元素还是一个集合

2.1 List嵌套list

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
ArrayList<String> class1 = new ArrayList<String>();
Collections.addAll(class1, "a", "b", "c");

ArrayList<String> class2 = new ArrayList<String>();
Collections.addAll(class2, "e", "f", "g");

// 将class1和class2保存到一个大集合中
ArrayList<ArrayList<String>> classAll = new ArrayList<ArrayList<String>>();

Collections.addAll(classAll, class1, class2);

for (ArrayList<String> className : classAll) {
for (String name:className
) {
System.out.println(name);
}
System.out.println(className);
}
}

2.2 List嵌套Map
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
30

public static void main(String[] args) {
HashMap<String, Integer> class1 = new HashMap<String, Integer>();
class1.put("a", 12);
class1.put("b", 11);
class1.put("c", 13);
class1.put("d", 14);
class1.put("e", 18);
HashMap<String, Integer> class2 = new HashMap<String, Integer>();
class2.put("f", 19);
class2.put("g", 17);
class2.put("h", 16);
class2.put("i", 15);
class2.put("k", 14);

ArrayList<HashMap<String, Integer>> classAll = new ArrayList<HashMap<String, Integer>>();
classAll.add(class1);
classAll.add(class2);

System.out.println(classAll);

for (HashMap<String, Integer> className : classAll
) {
Set<String> keys = className.keySet();
for (String key : keys) {
System.out.println(key);
}
System.out.println(className);
}
}

2.3 Map嵌套Map
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
30
31
32
33
34
35
36
37
38
39
40
41

public static void main(String[] args) {
HashMap<String, Integer> class1 = new HashMap<String, Integer>();
class1.put("a", 12);
class1.put("b", 11);
class1.put("c", 13);
class1.put("d", 14);
class1.put("e", 18);
HashMap<String, Integer> class2 = new HashMap<String, Integer>();
class2.put("f", 19);
class2.put("g", 17);
class2.put("h", 16);
class2.put("i", 15);
class2.put("k", 14);

//将两个班级的Map集合保存到另一个集合中,要求有该班级的名字
HashMap<String, HashMap<String, Integer>> classAll = new HashMap<String, HashMap<String, Integer>>();

classAll.put("class one", class1);
classAll.put("class two", class2);

System.out.println(classAll);

//获取所有的键
Set<String> names = classAll.keySet();

System.out.println(names);
//遍历所有的键
for (String name : names) {
//以值找值
HashMap<String, Integer> map = classAll.get(name);
//获取map所有的键
System.out.println(map);
Set<String> ns = map.keySet();
for (String n : ns
) {
Integer value = map.get(n);
System.out.println(n + "..." + value);
}
}
}

.模拟斗地主洗牌发牌
3.1 案例介绍
3.2 案例分析
斗地主要想办法自定义排序规则
核心思想:给斗地主中每一张牌,准备一个编号
a.不能出现相同的编号
b.牌越大,编号越大

I.准备编号和牌组成的Map集合
II.准备54个编号(一副牌)
III.洗牌(打乱集合)
V.发牌(遍历集合)
IV.对编号排序(sort)
IIV.转牌(以键找值map.get)
IIIV.打印给用户(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

public static void main(String[] args) {
// I.准备编号和牌组成的Map集合
LinkedHashMap<Integer, String> Map = new LinkedHashMap<Integer, String>();

//a.花色4种
String[] colors = {"黑桃♠️", "红桃♥️", "梅花♣️", "方片♦️"};
//b.数值13种
String[] numbers = {"3", "4", "5", "6", "7", "8", "9", "10", "J", "K", "Q", "A", "2"};
//c.组合拍
int id = 1;
for (String num : numbers) {
for (String color : colors) {
String card = color + num;
// for ( i < num.length(); i++) {
Map.put(id, card);
id++;
}
}
Map.put(53, "小王");
Map.put(54, "大王");

// II.准备54个编号(一副牌)
ArrayList<Integer> Cards = new ArrayList<Integer>();
for (int i = 1; i < 55; i++) {
Cards.add(i);
}
// III.洗牌(打乱集合)
Collections.shuffle(Cards);

// V.发牌(遍历集合)
ArrayList<Integer> player1 = new ArrayList<Integer>();
ArrayList<Integer> player2 = new ArrayList<Integer>();
ArrayList<Integer> player3 = new ArrayList<Integer>();
ArrayList<Integer> Holo_Card = new ArrayList<Integer>();

// IV.对编号排序(sort)
//此处不能使用增强for
for (int i = 0; i < Cards.size()-3; i++) {
Integer Card = Cards.get(i);
if (i % 3 == 0) {
player1.add(Card);
} else if (i % 3 == 1) {
player2.add(Card);
} else {
player3.add(Card);
}
}
//最后3张给底牌
Holo_Card.add(Cards.get(53));
Holo_Card.add(Cards.get(52));
Holo_Card.add(Cards.get(51));

Collections.sort(player1);
Collections.sort(player2);
Collections.sort(player3);
Collections.sort(Holo_Card);

// IIV.转牌(以键找值map.get)
// IIIV.打印给用户(println)
lookCards(player1, Map);
lookCards(player2, Map);
lookCards(player3, Map);
lookCards(Holo_Card, Map);
}

public static void lookCards(ArrayList<Integer> idCards, LinkedHashMap<Integer, String> Map) {
for (Integer idCard : idCards) {
String card = Map.get(idCard);
System.out.println(card + "");
}
System.out.println();
}

3.3 代码实现

三.冒泡排序
排序:将一组数据按照固定的规则进行排列

3.1 冒泡排序的介绍
所谓的冒泡排序的思想:
依次比较数组/集合中相连的两个元素,然后将较大的元素放在后面,最后按照从小到大的顺序排列出来
规律:
•如果有n个数进行排序比较,那么比较次数为n-1轮
•每轮比较完毕,下一轮比较就会少一个数据参与

4.2 冒泡排序过程图解

3.3 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
int[] arr = {4, 6, 1, 3, 82, 9, 7, 5};
//外层循环,控制循环次数
for (int i = 0; i < arr.length - 1; i++) {
//内层循环,控制比较次数
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
System.out.println(Arrays.toString(arr));
}

总结:
1.Map接口中定义对通用方法*
put(K,V);
remove(K);
get(K)
put(repeat K,V)
containsKey(K)
containsValue(V)

2.Map各种实现类对遍历方式(以键找值,键值对)*
a.以键找值
set keys = map.kepSet();
for(K key:keys){//遍历所有的键
//以键找值
V value = map.get(key);
sout(key,value);
}

b.键值对
set<Map.Entry<K,V>> entrys = map.entrySet();//获取所有的键值对集合
for(Map.Entry<K,V> entry:entrys)//遍历键值对集合
{
K key = entry.getKey();//获取键值中对键和值
V value = entry.getValue();//获取键值中对键和值
sout(key,value);
}
3.集合嵌套
a.List套list
ArrayList arr;

b.List套Map
ArrayList<HashMap<String,Integet>> arr;

c.Map套map
HashMap<String,HashMap<String,Integer>> map;

4.斗地主案例*

5.冒泡排序
a.冒泡过程
b.算法背下来

1
2
3
4
5
6
7
8
for(int i=0;i<arr.lenth-1;i++){
for(int j=0;j<arr.lenth-1-i;j++){
if(arr[j]>arr[j+i]){
int temp=arr[j];
arr[j]=arr[j+1]
arr[j+1]=temp;
}}
}

一.List接口
1.1 List接口的特点
a.List接口 继承 Collection接口
b.List接口的特点:
有序的(Java中的有序不是指自然顺序,而是指存取元素的顺序是一致的,什么顺序存就怎么取)
有索引的
元素可重复的(存储的元素可以重复)

1.2 List接口中常用的方法以及常用实现类
a.List接口继承Collection接口,所以已经有了collection中的8个(7个常见+iterator)
b.List接口还有自己的特有方法(4个):
增:add(int index,E e)
删:remove(int index,E e)
改:set(int index,E e)
查:get(int index,E e)
c.List接口有哪些实现类
ArrayList[]
LinkedList[]
Vector[]

1.3 ArrayList的数据结构以及使用
a.ArrayList集合的方法(7个collection+1个迭代器+4个List接口中的)
特有方法:无
b.ArrayList集合底层采用的是数组结构,特点:查改快,增删慢
c.使用ArrayList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
ArrayList<String> arr = new ArrayList<String>();
arr.add("Java1" );
arr.add("Java2" );
arr.add("Java3" );
arr.add(2, "99");
System.out.println(arr);

arr.remove(3);
System.out.println(arr);

arr.set(1, "11");
System.out.println(arr);

System.out.println(arr.get(2));
}

List的并发修改异常
并发修改异常:ConcurrentModificationException
•ConcurrentModificationException
产生原因:
•迭代器遍历的过程中,通过集合对象修改了集合中元素的长度,造成类迭代器获取元素中判断欲求修改值和实际修改值不一致
解决方案:
•用for循环遍历,然后用集合对象做对应的操作即可(get方法)

ListIterator:列表迭代器
•通过List集合的listIterator()方法得到,它是List集合特有的迭代器
•用于允许程序员沿任一方向遍历列表的列表迭代器,在迭代期间修改列表,并获取列表中迭代器的当前位置

ListIterator中的常用方法
•E next();返回迭代中的下一个元素
•boolean hasNext();如果迭代器具有更多元素,则返回true
•E previous();返回迭代中的上一个元素
•boolean hasPrevious();如果此列表迭代器在相反方向遍历列表时具有更多元素,则返回true
•void add(E e);将指定的元素插入列表

1.4 LinkedList的数据结构以及使用
a.LinkedList的方法(7个collection+1个迭代器+4个List接口中的)
有特有方法:
public void addFirst(E e);//添加元素到集合首
public void addLast(E e);//添加元素到集合尾
public E getFirst(E e);//获取集合的首元素
public E getLast(E e);//获取集合的尾元素
public E removeFirst(E e);//删除集合的首元素
public E removeLast(E e);//删除集合的尾元素

public void pop(E e);//删除集合中的首元素,底层就是removeFirst
public void push(E e);//添加集合中的首元素,底层就是addFirst
源码:

1
2
3
4
5
6
7
public E pop() {
return removeFirst();
}

public void push(E e) {
addFirst(e);
}

b.LinkedList底层采用的是链表结构,特点:查改慢,增删快
c.使用LinkedList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
LinkedList<String> list = new LinkedList<String >();
list.add("a");
list.add("b");
list.add("c");

System.out.println(list);

list.addFirst("1");
list.addLast("10");

System.out.println(list);

System.out.println(list.getFirst());
System.out.println(list.getLast());

list.removeFirst();
System.out.println(list);

list.removeLast();
System.out.println(list);

list.pop();
System.out.println(list);

list.push("2");
System.out.println(list);

1.5 LinkedList的源码分析
a.LinkedList底层采用的是链表结构(双向列表)

b.LinkedList这个类有两个成员变量
Node first;记录了开始结点
Node last;记录了结束结点

c.结点类Node,它是LinkedList的内部类

1
2
3
4
private static class Node<E> {
E item;//该节点的数据域
Node<E> next;//指针域,指向下一个结点
Node<E> prev;//指针域,指向上一个结点

d.LinkedList的add方法
I.将新增的结点,添加到last之后
II.并且将size++,总元素个数加一

1
2
3
4
public boolean add(E e) {
linkLast(e);
return true;
}

e.LinkedList的get方法
I.先查找指定索引的结点,(从前往后找,从后往前找)
II.找到结点后,获取结点的数据栈,然后返回

完整版:
•LinkedList的成员变量源码分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
transient int size = 0;
/**
* 存储第一个节点的引用
*/
transient Node<E> first;
/**
* 存储最后一个节点的引用
*/
transient Node<E> last;
//......
//......
}

•LinkedList的内部类Node类源码分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
//......
private static class Node<E> {
E item;//被存储的对象
Node<E> next;//下一个节点
Node<E> prev;//前一个节点

//构造方法
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
//......
}

•LinkedList的add()方法源码分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public boolean add(E e) {
linkLast(e);//调用linkLast()方法
return true;//永远返回true
}

void linkLast(E e) {
final Node<E> l = last;//一个临时变量,存储最后一个节点
final Node<E> newNode = new Node<>(l, e, null);//创建一个Node对象
last = newNode;//将新Node对象存储到last
if (l == null)//如果没有最后一个元素,说明当前是第一个节点
first = newNode;//将新节点存为第一个节点 else
l.next = newNode;//否则不是第一个节点,就赋值到当前的last的next成员
size++;//总数量 + 1
modCount++;//
}

•LinkedList的get()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public E get(int index) {
checkElementIndex(index);//检查索引的合法性(必须在0-size之间),如果不合法,此方法抛出异常
return node(index).item;
}

Node<E> node(int index) {//此方法接收一个索引,返回一个Node
// assert isElementIndex(index);
if (index < (size >> 1)) {//判断要查找的index是否小于size / 2,二分法查找
Node<E> x = first;// x = 第一个节点——从前往后找
for (int i = 0; i < index; i++)//从0开始,条件:i < index,此循环只控制次数
x = x.next;//每次 x = 当前节点.next;
return x;//循环完毕,x就是index索引的节点。
} else {
Node<E> x = last;// x = 最后一个节点——从后往前找
for (int i = size - 1; i > index; i--)//从最后位置开始,条件:i > index
x = x.prev;//每次 x = 当前节点.prev;
return x;//循环完毕,x就是index索引的节点
}
}

二.Collections类
2.1 Collections的介绍
Collections不是collection!
作用:Collections是一个集合的工具类,该类中有一堆静态方法,专门操作集合

2.2 Collections常用功能
public static void shuffle(List list);//随机打乱集合中元素的顺序 public static void sort(List list);//将集合中的元素进行升序排序

扩展:Collections的sort方法,默认是对集合中对元素进行升序排序
如果集合的泛型是数值类型,那么按照数值的大小升序
如果集合的泛型是Character类型,那么按照字母的码值排序
如果集合的泛型是String类型,那么按照字母首字母排序,若一样,次字母排序,依次类推
提示:Collections.sort方法结论和Array.sort()是一样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args) {
ArrayList<Integer> arr = new ArrayList<Integer>();

arr.add(1);
arr.add(2);
arr.add(3);
arr.add(5);
arr.add(9);
arr.add(8);
arr.add(7);
arr.add(6);
arr.add(4);

System.out.println(arr);

Collections.sort(arr);
System.out.println(arr);

Collections.shuffle(arr);
System.out.println(arr);
}

2.3 Comparator比较器排序
Collections还有一个sort方法:
public static void sort(List list,Comparator com);带有比较器的排序方法
这个比较器源码:

1
2
3
public interface Comparator<T>{
int compare(T o1, T o2);
}

我们注意到Comparator实际上是一个接口,那么我们在调用sort方法时,需要传人一个该接口的实现类对象(匿名内部类)

降序排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Collections.sort(arr, new Comparator<Integer>() {
/*该方法就是比较方法,它会把集合中每两个元素传入
@param o1 第一个元素
@param o2 第二个元素
@return 返回值代表谁大谁小,如果是正数代表o1大,负数则代表o2大,如果0代表一样大

口诀:升序前减后,降序后减前
*/

@Override
public int compare(Integer o1, Integer o2) {
return o2-o1;//降序
//return o1-o2; //升序

}
});

自定义排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static void main(String[] args) {
ArrayList<Dog> arr = new ArrayList<Dog>();

arr.add(new Dog(1, "a", 4));
arr.add(new Dog(2, "b", 2));
arr.add(new Dog(3, "c", 3));
arr.add(new Dog(4, "d", 2));

Collections.sort(arr, new Comparator<Dog>() {
@Override
public int compare(Dog o1, Dog o2) {
//按照年龄降序排序
// return o2.age - o1.age;
//按照姓名升序排序
// return o1.name.length() - o2.name.length();
//按照年龄和腿数总和升序
return (o1.age+o1.legs_num)-(o2.age+o2.legs_num);

}
});

for (Dog dog : arr) {
System.out.println(dog);

}
}

2.4 可变参数
a.什么是可变参数:是指方法参数的个数可以任意[JDK 1.5]
b.可变参数的格式:
数据类型 … 变量名

1
public static int getSum(int … a){}
可变参数的本质其实就是一个数组;
注意事项:
a.一个方法中最多只能有一个可变参数
b.一个方法中如果既有正常参数,又有可变参数,那么可变参数必须写在最后面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
System.out.println(getSum());
System.out.println(getSum(1));
System.out.println(getSum(1, 2));
System.out.println(getSum(1, 2, 3));
System.out.println(getSum(1, 2, 3, 4));
System.out.println(getSum(1, 2, 3, 4, 5));
}

public static int getSum(int... a) {
System.out.println("length: "+a.length);
int sum = 0;
for (int i : a) {
sum +=i;
}
return sum;
}

2.5 可变参数的应用场景
Arrays工具类中有一个静态方法:
•public static List asList<T…a>:返回由指定数组支持的固定大小的列表
•返回的集合不能做 增 删 操作,可以做修改操作

List接口中有一个静态方法:
•public static List of(E…elements):返回包含任意数量元素的不可变列表
•返回的集合不能做 增 删 该 操作

Set接口中有一个静态方法:
•public static Set of(E…elements):返回一个包含任意数量元素的不可变集合
•在给元素时,不能给重复的元素

例:

1
2
3
4
5
6
7
8

public static void main(String[] args) {
ArrayList<String > arr= new ArrayList<String>();
//Collections工具类又有一个方法,叫做addAll
Collections.addAll(arr,"a","b","c","d","e");

System.out.println(arr);
}

一次性向集合中添加多个元素

三.Set接口
3.1 Set接口的特点
a.Set接口也是继承Collection接口
b.Set接口的特点:
无序的(指存取顺序不保证一致)
无索引的(使用不能使用普通for循环遍历)
元素唯一的

但是实际上,严格来说,无序特点是不对的(实现类LinkedHashSet是有序的)
3.2 Set接口的常用方法以及常用实现类
a.Set接口中的方法(7个collection+1个迭代器)
b.Set方法的特有方法:无

c.Set接口的实现类:
HashSet
LinkedHashSet
TreeSet
3.3 HashSet的数据结构以及使用
a.HashSet也没有特有方法
b.HashSet底层采用的是哈希表结构(数组结构+链表结构+红黑树结构)
•对集合的迭代顺序不作任何保证,也就是说不保证存储和取出的元素 顺序一致
•没有带索引的方法,索引不能使用普通for循环遍历
•Set集合,所以是不包含重复元素的集合
c.HashSet的使用

1
2
3
4
5
6
7
8

public static void main(String[] args) {
HashSet<String> set = new HashSet<String>();
Collections.addAll(set, "php","Java","C#");
Collections.addAll(set, "Java","C#","Delphi","C","CPP","Python");

System.out.println(set);
}

3.4 LinkedHashSet的数据结构以及使用
a.LinkedHashSet也没有特有方法
b.LinkedHashSet底层采用的是(链表结构+哈希表结构)
•由链表保证元素有序,也就是说元素的存储和取出顺序是一样的
•由哈希表保证元素唯一,也就是说没有重复的元素

c.LinkedHashSet的使用

1
2
3
4
5
6
7
public static void main(String[] args) {
LinkedHashSet LHS = new LinkedHashSet<String>();
Collections.addAll(LHS, "php", "Java", "C#");
Collections.addAll(LHS, "Java", "C#", "Delphi", "C", "CPP", "Python");

System.out.println(LHS);
}

3.5 TreeSet的数据结构以及使用
a.TreeSet特点
无序的(无序是存取顺序不保证一致,但是它会以自然顺序输出)
TreeSet是无序的,但是它是无序中的一种特殊存在,有自然顺序!
TreeSet(Comparator comparator):根据指定的比较器进行排序
无索引,所以不能用普通for循环
元素唯一,因为是Set集合
b.TreeSet特有方法:无
c.TreeSet底层采用的是红黑树结构
d.TreeSet的使用

1
2
3
4
5
6
7
8
9
10
TreeSet<Integer> tree = new TreeSet<Integer>();
//添加元素,没有带索引的方法,证明无索引
//tree.add()
Collections.addAll(tree, 1, 3, 5, 2, 4, 6);
System.out.println(tree);
//[1, 2, 3, 4, 5, 6]证明自然顺序

tree.add(4);
System.out.println(tree);
//[1, 2, 3, 4, 5, 6] 证明元素唯一性

扩展:
如果TreeSet保存的是数值类型,那么按照数值的大小升序
如果TreeSet保存是Character类型,那么按照字母的码值排序
如果TreeSet保存的是String类型,那么按照字母首字母排序,若一样,次字母排序,依次类推

e.TreeSet也可以使用比较器自定义排列顺序
TreeSet有一个带有比较器的构造方法

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
TreeSet<Integer> tree = new TreeSet<Integer>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1;
}
});
Collections.addAll(tree, 1, 3, 5, 2, 4, 6);
System.out.println(tree);
}

3.6 哈希表结构的的介绍
•对象的哈希值(对象的”数字指纹”,是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值)
a.对象的哈希值,相对于对象的“指纹”,只是这个指纹是一个数字
b.我们如何获取对象的哈希值:调用对象的hashCode()方法即可

1
2
3
4
5
public static void main(String[] args) {
Student s1 = new Student(1, “a”);
int hashCode = s1.hashCode();//返回哈希值
System.out.println(hashCode);
}
c.其实Java中所谓的地址值是假的,它是Hash值的16进制表示
源码

1
2
3
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

d.Java中有真正的地址值,但是当打印时,会自动调用toString方法,将hash值输出
Dog d = new Dog();//d就是真正的地址值
System.out.println(d);//打印时,会自动调toString方法,hex16操作输出

e.不同的两个对象,可能有相同的hashCode(hashCode是int类型,最多42亿,因此会相同)

结论:哈希表结构如何保证元素的唯一性
哈希表结构,如何判断两个元素是否重复:
a.哈希表结构,会先比较两个对象的哈希值
b.当哈希值一样,再调用equals比较两个对象
只有哈希值相同,并且equals结构为true,才判定这两个元素是重复的

哈希表结构=数组结构+链表结构+红黑树结构(JDK8)
数组结构默认长度16
I.向哈希值中添加元素时,元素的索引:元素.hashCode()%数组长度 =0-15
II.当某个链表长度超过阀值(8)时,这个链表就会变成红黑树

3.7 哈希表结构保存自定义类型练习
为了保证哈希表中元素的唯一性,如果元素是自定义类型,那么必须重写hashCode和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
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
public class Dog {
int age;
String name;
int legs_num;

public Dog(){

}

public Dog(int age, String name, int legs_num) {
this.age = age;
this.name = name;
this.legs_num = legs_num;
}

@Override
public String toString() {
return "Dog{" +
"age=" + age +
", name='" + name + '\'' +
", legs_num=" + legs_num +
'}';
}
//为了保证哈希表中元素的唯一性,要重写hasCode和equals方法

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Dog dog = (Dog) o;
return age == dog.age &&
legs_num == dog.legs_num &&
Objects.equals(name, dog.name);
}

@Override
public int hashCode() {
return Objects.hash(age, name, legs_num);
}
}

public class TestHashSetDemo {
public static void main(String[] args) {
HashSet<Dog> dogHashSet = new HashSet<Dog>();
dogHashSet.add(new Dog(1, "a", 4));
dogHashSet.add(new Dog(2, "b", 4));
dogHashSet.add(new Dog(3, "c", 4));
dogHashSet.add(new Dog(1, "d", 4));

// for (Dog d : dogHashSet) {
// System.out.println(d);
// }
// System.out.println();
//
// dogHashSet.add(new Dog(3, "c", 4));
// //哈希表判断两个元素重复or不重复的依据是哈希表和equals
// for (Dog d : dogHashSet) {
// System.out.println(d);
// }

//为了保证元素的唯一性,要重写hashCode和equals,根据内容计算哈希值,equals也比较内容

System.out.println();
for (Dog d:dogHashSet
) {
System.out.println(d);
}
}

3.8 HashSet的源码分析
a.构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class HashSet<E> extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
//内部一个HashMap——HashSet内部实际上是用HashMap实现的 private transient HashMap<E,Object> map;
// 用于做map的值
private static final Object PRESENT = new Object();

/**
* 构造一个新的HashSet,
* 内部实际上是构造了一个HashMap
*/
public HashSet() {
map = new HashMap<>();
}
}

b.HashMap的add方法

1
2
3
4
5
6
7
public class HashSet { //......
public boolean add(E e) {
return map.put(e, PRESENT) == null;//内部实际上添加到map中,键:要添加的对象,值:Object
对象
}
//......
}

c.HashMap的put方法
HashMap保存键时,是以键的哈希值为依据确定键的保存位置
添加成功之后,size++,将元素的个数增加1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class HashMap { //......
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

//......
static final int hash(Object key) {//根据参数,产生一个哈希值
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

//......
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {

Node<K, V>[] tab; //临时变量,存储"哈希表"——由此可见,哈希表是一个Node[]数组
Node<K,V> p;//临时变量,用于存储从"哈希表"中获取的Node
int n, i;//n存储哈希表长度;i存储哈希表索引
if ((tab = table) == null || (n = tab.length) == 0)//判断当前是否还没有生成哈希表
n = (tab = resize()).length;//resize()方法用于生成一个哈希表,默认长度:16,赋给n
if ((p = tab[i = (n - 1) & hash]) == null)//(n-1)&hash等效于hash % n,转换为数组索
tab[i] = newNode(hash, key, value, null);//此位置没有元素,直接存储
else{//否则此位置已经有元素了
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))//判断哈希值和
e = p;//将哈希表中的元素存储为e
else if (p instanceof TreeNode)//判断是否为"树"结构
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
else {//排除以上两种情况,将其存为新的Node节点
for (int binCount = 0; ; ++binCount) {//遍历链表
if ((e = p.next) == null) {//找到最后一个节点
p.next = newNode(hash, key, value, null);//产生一个新节点,赋值到链
if (binCount >= TREEIFY_THRESHOLD - 1) //判断链表长度是否大于了8
treeifyBin(tab, hash);//树形化
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))//跟当前
变量的元素比较,如果hashCode相同,equals也相同 break;//结束循环
p = e;//将p设为当前遍历的Node节点 }
}
if (e != null) { // 如果存在此键
}
}

总结:

image

Collection
{
Iterator it = cc.iterator();
public boolean add(E e);添加元素,返回值表示是否添加成功。
public boolean remove(E e);删除元素,返回值表示是否删除成功。
无修改方法。
无查询方法。
public boolean contains(Object obj);判断集合中是否包含某个元素。
public void clear();清空集合(把集合中的元素全部删除,不是把集合置为Null)
public boolean isEmpty();判断集合是否为空(指集合中没有元素,而非集合是否为Null)
public int size();返回集合中元素的个数
public Object[] toArray();将集合转成数组
}
List接口:
特点:有序,有索引,元素可重复
特有方法:
add(int index,E e);
remove(int index,E e);
set(int index,E e);
get(int index,E e);

ArrayList:
底层是数组结构
特有方法:无
特点:有序,有索引,元素可重复

LinkedList
底层是链表结构
特有方法:
public void addFirst(E e);//添加元素到集合首
public void addLast(E e);//添加元素到集合尾
public E getFirst(E e);//获取集合的首元素
public E getLast(E e);//获取集合的尾元素
public E removeFirst(E e);//删除集合的首元素
public E removeLast(E e);//删除集合的尾元素
public void pop(E e);//删除集合中的首元素,底层就是removeFirst
public void push(E e);//添加集合中的首元素,底层就是addFirst
特点:有序,有索引,元素可重复

Set接口:
特点:无序(LinkedHashSet除外),无索引,元素唯一
特有方法:无
特点:无索引,元素唯一

HashSet
底层是哈希表结构(数组结构+链表结构+红黑树结构)
特有方法:无
特点:无序,无索引,元素唯一

LinkedHashSet
底层是(链表结构+哈希表结构)
特有方法:无
特点:有序,无索引,元素唯一

TreeSet
底层是红黑树结构
特有方法:无
特点:无序的(无序是存取顺序不保证一致,但是它会以自然顺序输出),无索引,元素唯一

使用哈希表保存自定义类型时,为了保证元素的唯一性,要重写自定义类型中的hashCode方法和equals方法

集合体系结构:
image

一.Collection集合(所有集合的根接口)
是单列集合的顶层接口,它表示一组对象,这些对象也称为Collection的元素
JDK不提供此接口的任何直接实现,它提供了更具体的子接口(如Set和List)实现
创建Collection集合的对象
•多态的方式
•具体的实现类ArrayList

1.集合的介绍&集合和数组的区别
a.什么是集合
集合就是Java用来保存数据的容器。
b.学过的容器
数组,ArrayList
数组定义: 数据类型[] 变量名 = new 数据类型[数组的长度]
集合定义: ArrayList<数据类型> 变量名 = new ArrayList<数据类型>();
c.数组和集合区别在哪里
I.数组的长度固定,集合的长度可变
II.数组中的元素类型可以是基本类型,也可以是引用类型。
集合中的元素类型必须只能是引用类型,如果想保存基本类型,要写该基本类型对应的写包装类。
例如:
ArrayList arr = new ArrayList();

2.集合框架的继承体系

List接口特点:
有序,有索引,元素可以重复

Set接口特点:
无序,无索引,元素不可以重复

image

3.Collection中的通用方法
增:增加
public boolean add(E e);添加元素,返回值表示是否添加成功。
删:删除
public boolean remove(E e);删除元素,返回值表示是否删除成功。
改:修改
无修改方法。
查:查询
无查询方法。
其他方法:
public boolean contains(Object obj);判断集合中是否包含某个元素。
public void clear();清空集合(把集合中的元素全部删除,不是把集合置为Null)
public boolean isEmpty();判断集合是否为空(指集合中没有元素,而非集合是否为Null)
public int size();返回集合中元素的个数
public Object[] toArray();将集合转成数组

方法名 说明
boolean add(E e) 添加元素
boolean remove(Object o) 从集合中移除指定元素
void clear() 清空集合中的元素
boolean contains(Object o) 判断集合中是否存在指定的元素
boolean isEmpty() 判断集合是否为空
int size() 集合的长度,也就是集合中的元素个数
public Object[] toArray(); 将集合转成数组
二.Iterator迭代器(遍历集合)
1.集合迭代器的介绍与使用
a.迭代器(iterator)
用于遍历集合的对象(集合有无索引,均可使用迭代器来遍历,迭代器遍历集合时不需要索引)
•Iterator iterator():返回此集合中元素的迭代器,通过集合的iterator()方法得到
•迭代器是通过集合的iterator()方法得到的,所以说他是依赖于集合而存在的

b.迭代器的使用
I.获取要遍历集合的迭代器对象
II.调用迭代器对象.hasNext();boolean类型
III.调用迭代器对象的.next();方法
1
2
3
4
5
6
7
8
9
10
Iterator it = cc.iterator();
// boolean b = a.hasNext();
// if (b) {
// String next = a.next();
// System.out.println(next);
// }
while(it.hasNext()){
String ss = it.next();
System.out.println(ss);
}
•迭代器的注意事项(2个异常)
a.NoSuchElementException
出现原因:迭代器的hasNext返回false,再调用next方法,就会返回此异常
b.ConcurrentModificationException
出现原因:Java的迭代器规定,使用迭代器过程中,不能向集合中增删元素(不能影响集合的长度),否则就会抛出并发修改异常。

结论:
a.使用迭代器时,必须先判断,且判断结果为true,才能调用.next()方法
b.使用迭代器时,应该尽量只做遍历(绝对不允许直接向集合中增删元素),即使用单纯遍历功能

2.迭代器的原理
迭代器的底层使用指针原理,迭代器输出的是内存地址,在地址块中寻找下一个,超出后提出越界,即返回NoSuchElementException错误。同理,地址不可变,使用增删会提示ConcurrentModificationException错误

3.增强for循环
a.什么是增强for循环
实际上是一种简便格式(语法糖),简化数组和Collection集合的遍历,实际上是迭代器的简便格式
b.增强for循环的用法
for(数据类型 变量名:集合/数组){}
c.增强for循环本质使用的就是迭代器
证明:
a.源码
b.如果在使用增强for的过程中向集合添加或删除元素,和迭代器一样会抛出ConcurrentModificationException异常

注意:和使用迭代器一样,增强for就是用于单纯的遍历集合,不要在遍历集合的过程中增删元素

三.泛型
1.什么是泛型
泛型是JDK1.5的新特性,提供了编译时类型安全检测机制,该机制允许在编译时见到到非法到类型
它的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数
参数化类型:就是将类型由原来的具体到类型参数化,然后在使用/调用时传入具体到类型

比如:
在JDK1.5之前,创建集合:ArrayList arr = new ArrayList();集合中可以保存任意对象
在JDK1.5时,创建集合:ArrayList<具体的引用类型> arr = new new ArrayList<具体的引用类型>();
这种参数类型可以用在类、方法和接口中,分别被称为泛型类,泛型方法,泛型接口

什么是泛型:
是一种不确定的类型,程序员使用时再确定下来
泛型的格式:
,一个泛型,如Element。这里的类型可以看作形参
<K,V>,两个泛型,暂不确定类型。这里的类型可以看作形参
将来具体调用的适合给定的类型可以看作是实参,并且实参的类型只能是引用数据类型

2.泛型的好处
a.避免了强制转型的麻烦
b.避免了类型转换的异常,将运行时期的ClassCastException,转移到了编译时期的编译失败

总之:JDK1.5之后,Java强烈推荐使用泛型

3.泛型的定义和使用
泛型可以定义在类、方法、接口上

•泛型类
泛型可以定义在类上

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyArrayList {
//就是把E当作某种类型使用
private E ee;

public E getEe() {
    return ee;
}

public void setEe(E ee) {
    this.ee = ee;
}

}

泛型类的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TestMyArrayList {
public static void main(String[] args) {
//使用泛型类,MyArrayList
MyArrayList arr = new MyArrayList();
// 此时arr中集合对象没有E了,所有E都变成了String
arr.setEe(“Java”);
String ee = arr.getEe();
System.out.println(ee);

// 使用泛型类,MyArrayList
MyArrayList arr1= new MyArrayList();
arr1.setEe(1);
System.out.println(arr1.getEe());
}
}
•泛型方法
泛型方法的定义:

1
2
3
4
5
public class Dog {
public void show(E num){
System.out.println(num);
}
}
泛型方法的使用:

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
//create object
Dog dd = new Dog();
//use method
dd.show(“Java”);
dd.show(10);
//严格的方式
dd.show(“java”);
dd.show(10);
}
•泛型接口
泛型定义在某个接口上

1
2
3
4
public interface MyInterface {
public abstract void show(E ee) ;
public abstract void eat(E ee) ;
}
1
2
3
4
5
6
7
8
9
10
//a.定义一个实现类,实现该接口时,可以确定E的具体类型
public class MyClass implements MyInterface{
@Override
public void show(String ee) {
}

@Override
public void eat(String ee) {
}

}
1
2
3
4
5
6
7
8
9
10
11
public class MyClass2 implements MyInterface{
@Override
public void show(E ee) {
}

@Override
public void eat(E ee) {
}

}
//此时实现类就是一个泛型类,创建该类对象时,可以确定泛型的具体类型

丢弃泛型(不正规),此时泛型被丢弃,所有泛型变为Object

1
2
3
4
5
6
7
8
9
10
11
public class MyClass3 implements MyInterface{
@Override
public void show(Object ee) {

}

@Override
public void eat(Object ee) {

}

}
总结:在开发中,很少自定义泛型类、方法、接口,大概率是使用已定义好的泛型类,自行更改类型即可

4.泛型通配符
什么是泛型通配符:
当集合中泛型不确定类型时,可以使用泛型通配符,表示何种泛型均可

•类型通配符: •List:表示元素类型未知的List,它的元素可以匹配任何的类型
•这种带通配符的List仅表示它是各种泛型List的父类,并不能把元素添加进去

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
ArrayList arr1 = new ArrayList();
ArrayList arr2 = new ArrayList();
ArrayList arr3 = new ArrayList();

method(arr1);
method(arr2);
method(arr3);

}

public static void method(ArrayList<?> arr) {

}
5.泛型的上下限
<?>什么泛型都是OK的

如果不希望List<?>是任何泛型List的父类,只希望它代表某一类泛型List的父类,可以使用类型通配符的上线

,叫泛型的上限,表示该泛型必须是Animal本类或其子类(Dog,Cat); 可以理解为”” .叫泛型的下限,表示该泛型必须是Student本类或其父类(Person,Object) 可以理解为”
=Student>”

范例:

1
2
3
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 static void main(String[] args) {
Collection list1 = new ArrayList();
Collection list2 = new ArrayList();
Collection list3 = new ArrayList();
Collection list4 = new ArrayList();
//Number是8种数值类型的父类
//SuperClass:Object—subclass(String,Number)—subclass(Integer,Number)

    getElement1(list1);

// getElement1(list2);String和Number不是super-sub关系,不行
getElement1(list3);
// getElement1(list4);Object最大,不行

// getElement2(list1);Integer小于Number,不行
// getElement2(list2);String和Number不是Super-sub关系,不行
getElement2(list3);
getElement2(list4);
}

public static void getElement1(Collection<? extends Number> collection ) {

}

public static void getElement2(Collection<? super Number> collection ) {

}

}
四.数据结构
1.什么是数据结构
数据结构是计算机存储、组织数据的方式(容器保存数据的方式)。是指相互之间存在一种或多种特定关系的数据元素的集合

2.常见的4+1种数据结构
掌握前4种数据结构
•栈结构
可以看成只有一端开口的容器
入栈/压栈->[栈顶U栈底]->出栈/弹栈
入栈顺序:1 2 3
出栈顺序:3 2 1

特点:先进后出

•队列结构
两端均有开口的容器
入队->[队尾——队头]->出队

只能从队尾进

入队顺序:1 2 3 4
出队顺序:1 2 3 4

特点:先进先出FIFO(First In First Out)

•数组结构
数组结构是连续的一块区域,每个数组都有自己的索引

获取/修改元素:根据索引找到/修改元素即可

添加元素经过:
a.创建新数组
b.复制老数组数据
c.添加新的数据
d.销毁老的数据

删除元素经过:
a.创建新数组
b.复制老数组数据
c.删除指定的数据
d.销毁老的数据

特点:增删慢,查改快

•链表结构
在链表结构中,每一个元素称为node(节点/结点),每个node至少有两部分内容

单向链表结构
双向链表结构

数据域|指针域(指向下一个node)
(指向上一个node)指针域|数据域|指针域 (指向下一个node)

image

获取/修改元素:从第一个元素开始往后查找(比数组结构慢)
增加/删除元素:在指针域直接添加/删除新node

特点:链表结构在内存中是可以分散的,增删快,查改慢

了解另一种数据结构
•树结构
树具有的特点:

每一个节点有零个或者多个子节点
没有父节点的节点称之为根节点,一个树最多有一个根节点。 3. 每一个非根节点有且只有一个父节点

image

名词 含义
结点 指树中的一个元素
结点的度 节点拥有的子树的个数,二叉树的度不大于2
叶子结点 度为0的节点,也称之为终端结点
高度 叶子结点的高度为1,叶子结点的父节点高度为2,以此类推,根节点的高度最高
层 根节点在第一层,以此类推
父节点 若一个节点含有子节点,则这个节点称之为其子节点的父节点
子结点 子节点是父节点的下一层节点
兄弟结点 拥有共同父节点的节点互称为兄弟节点
二叉树:如果树中的每个节点的子节点的个数不超过2,那么该树就是一个二叉树。

image

二叉查找树:
二叉查找树的特点:

左子树上所有的节点的值均小于等于他的根节点的值
右子树上所有的节点值均大于或者等于他的根节点的值
每一个子节点最多有两个子树

image

遍历获取元素的时候可以按照”左中右”的顺序进行遍历;
注意:二叉查找树存在的问题:会出现”瘸子”的现象,影响查询效率

image

二叉平衡树:
概述
为了避免出现”瘸子”的现象,减少树的高度,提高我们的搜素效率,又存在一种树的结构:”平衡二叉树” 规则:它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树

image

如图所示,上图是一棵平衡二叉树,根节点10,左右两子树的高度差是1,而下图,虽然根节点左右两子树高度 差是0,但是右子树15的左右子树高度差为2,不符合定义,
所以右图不是一棵平衡二叉树。

image

红黑树:

每一个节点或是红色的,或者是黑色的。
根节点必须是黑色
每个叶节点(Nil)是黑色的;(如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为Nil,这些
Nil视为叶节点)
如果某一个节点是红色,那么它的子节点必须是黑色(不能出现两个红色节点相连的情况)
对每一个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点;
红黑树的特点:
查询效率非常恐怖,增删速度非常慢

总结:
1.Collection集合根接口
I.集合继承体系
II.Collection中定义的通用方法
III.明白集合和数组的区别
2.迭代器
I.迭代器使用的三个步骤:获取迭代器,判断有没有下一个,取出下一个元素
II.增强for循环使用(底层使用就是迭代器)
III.迭代器和增强for使用过程,不能增删元素

3.泛型
I.泛型怎么写
II.泛型类、接口、方法怎么定义和使用
III.泛型通配符以及上下限
<?>代表任意泛型即可
<? extends X >上限
<? super Xxx>下限

4.数据结构
栈结构:先进后出
队列结构:先进先出
数组结构:增删慢,查改快
链表结构:增删快,查询慢

红黑树:查询效率非常高,增删速度非常慢