Java 基础(下篇)

Java 基础(下篇)


Collection 集合

集合概述

Java 基础(中篇) 已经学习过并使用过集合 ArrayList<E> ,那么集合到底是什么呢?集合是 java 中提供的一种容器,可以用来存储多个数据。那么集合和数组既然都是容器,它们之间有什么区别呢?

数组和集合的区别:
1、数组的长度是固定的。集合的长度是可变的。 2、数组中存储的是同一类型的元素,可以存储基本数据类型值,也可以存储对象。集合存储的都是对象,而且对象的类型可以不一致。在开发中一般当对象多的时候,使用集合进行存储。

来看下图,了解集合:

集合框架

JavaSE 提供了满足各种需求的 API,在使用这些 API 前,先了解其继承与接口操作架构,才能了解何时采用哪个类,以及类之间如何彼此合作,从而达到灵活应用。

集合按照其存储结构可以分为两大类,分别是单列集合 java.util.Collection 和双列集合 java.util.Map

Collection 集合:
1、Collection 集合:单列集合类的根接口,用于存储一系列符合某种规则的元素,它有两个重要的子接口,分别是 List 和 Set 。 2、List 集合:List 的特点是 元素有序、元素可重复。List 的主要实现类有 ArrayList 和 LinkedList。 3、Set 集合:Set 的特点是 元素无序,而且不可重复。Set 的主要实现类有 HashSet 和 TreeSet。

从上面的描述可以看出 JDK 中提供了丰富的集合类库,为了便于进行系统地学习,接下来通过一张图来描述整个集合类的继承体系。

其中,橙色框代表接口类型,而蓝色框代表具体的实现类。集合本身是一个工具,它存放在 java.util 包中。在 Collection 接口中定义着单列集合框架中最最共性的内容。

Collection 常用功能

Collection 是所有单列集合的父接口,因此在 Collection 中定义了单列集合( List 和 Set ) 通用 的一些方法,这些方法可用于操作所有的单列集合。这些通用方法如下:

  • public boolean add(E e): 把给定的对象添加到当前集合中 。
  • public void clear() :清空集合中所有的元素。
  • public boolean remove(E e): 把给定的对象在当前集合中删除。
  • public boolean contains(E e): 判断当前集合中是否包含给定的对象。
  • public boolean isEmpty(): 判断当前集合是否为空。
  • public int size(): 返回集合中元素的个数。
  • public Object[] toArray(): 把集合中的元素,存储到数组中。

代码实例演示:

1
2
3
4
5
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
// java.util.Collection 接口,所有单列集合的最顶层的接口,里边定义了所有单列集合共性的方法,任意的单列集合都可以使用 Collection 接口中的方法
public static void main(String[] args) {
// 01-创建集合对象,可以使用多态,只要实现了 Collection 接口的实现类都可以,如 ArrayList、hashSet、LinkedList 等等
Collection<String> collection = new ArrayList<>();
// Collection<String> coll = new ArrayList<>();
System.out.println(collection); // 结果:,说明重写了 toString 方法

// 02-添加操作,把给定的对象添加到当前集合中,返回值是一个 boolean 值,一般都返回 true ,所以可以不用接收
boolean bool01 = collection.add("张三");
System.out.println(bool01);
System.out.println(collection);
collection.add("李四");
collection.add("李四");
collection.add("王五");
collection.add("赵六");
System.out.println(collection);

// 03-删除操作,把给定的对象在当前集合中删除。返回值是一个 boolean 值,集合中存在元素,删除元素,返回 true,集合中不存在元素,删除失败,返回 false
boolean bool02 = collection.remove("Jack");
System.out.println(bool02); // 元素不存在,返回 false
boolean bool03 = collection.remove("李四");
System.out.println(bool03); // 元素存在,返回 true
System.out.println(collection);

// 04-是否包含,判断当前集合中是否包含给定的对象。包含返回 true,不包含返回 false
boolean bool04 = collection.contains("Jack");
System.out.println(bool04); // 不包含,返回 false
boolean bool05 = collection.contains("张三");
System.out.println(bool05); // 包含,返回 true

// 05-是否为空,集合为空返回 true,集合不为空返回 false
boolean isEmpty = collection.isEmpty();
System.out.println(isEmpty);

// 06-集合大小,返回集合中元素的个数。
int size = collection.size();
System.out.println(size);
System.out.println(collection);

// 07-集合转数组,把集合中的元素,存储到数组中。
Object[] objects = collection.toArray();
for (int i = 0; i < objects.length; i++) {
System.out.println(objects[i]);
}

// 08-清空集合操作,清空集合中所有的元素。但是不删除集合,集合还存在
collection.clear();
System.out.println(collection);
System.out.println(collection.isEmpty());
}

迭代器知识

迭代器介绍

在程序开发中,经常需要遍历集合中的所有元素。针对这种需求,JDK 专门提供了一个接口 java.util.Iterator。Iterator 接口也是 Java 集合中的一员,但它与 Collection、Map 接口有所不同,Collection 接口与 Map 接口主要用于存储元素,而 Iterator 主要用于迭代访问(即遍历)Collection 中的元素,因此 Iterator 对象也被称为迭代器。想要遍历 Collection 集合,那么就要获取该集合迭代器完成迭代操作。

迭代的概念:
迭代是集合元素的通用获取方式。在取元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来,继续在判断,如果还有就再取出出来。一直把集合中的所有元素全部取出。这种取出方式专业术语称为迭代。
Iterator 接口的常用方法如下:
  • public Iterator iterator(): 获取集合对应的迭代器,用来遍历集合中的元素的。
  • public E next():返回迭代的下一个元素。
  • public boolean hasNext():如果仍有元素可以迭代,则返回 true。

迭代器使用

接下来我们通过代码学习如何使用 Iterator 迭代集合中元素,使用代码把上述介绍的方法过一遍:

1
2
3
4
5
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 static void main(String[] args) {
// 01-创建集合对象
Collection<String> collection = new ArrayList<>();
// 02-往集合添加元素
collection.add("Lisa");
collection.add("Jack");
collection.add("Tom");
collection.add("Jerry");

// 03-创建迭代器,注意:Iterator<E> 接口也是有泛型的,迭代器的泛型跟着集合走,集合是什么泛型,迭代器就是什么泛型
Iterator<String> iterator = collection.iterator();

// 04-使用迭代器遍历集合
while (iterator.hasNext()){
System.out.println(iterator.next());
}

// 05-知识拓展:使用 for 遍历集合
System.out.println("===========");
// 这个 for 循环有点儿特殊
for (Iterator<String> it = collection.iterator(); it.hasNext();){
System.out.println(it.next());
}

// 06-如果集合中没有元素,那么会抛出异常 Exception in thread "main" java.util.NoSuchElementException
Collection<String> coll = new ArrayList<>();
Iterator<String> iterator1 = coll.iterator();
System.out.println(iterator1.next());
}

迭代器原理

我们在上述案例已经完成了 Iterator 遍历集合的整个过程。当遍历集合时,首先通过调用集合的 iterator() 方法获得迭代器对象,然后使用 hashNext() 方法判断集合中是否存在下一个元素,如果存在,则调用 next() 方法将元素取出,否则说明已到达了集合末尾,停止遍历元素。Iterator 迭代器对象在遍历集合时,内部采用指针的方式来跟踪集合中的元素,为了能更好地理解迭代器的工作原理,接下来通过一个图例来演示 Iterator 对象迭代元素的过程:

在调用 Iterator 的 next 方法之前,迭代器的索引位于第一个元素之前,即上图的 -1 位置,不指向任何元素,当第一次调用迭代器的 next 方法后,迭代器的索引会向后移动一位,指向第一个元素并将该元素返回,当再次调用 next 方法时,迭代器的索引会指向第二个元素并将该元素返回,依此类推,直到 hasNext 方法返回 false ,表示到达了集合的末尾,终止对元素的遍历。

增强 for 循环

所有的解释都在注释里面了,请看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 增强 for 循环(foreach) 特点:
* 1、增强 for 循环:底层使用的也是迭代器,使用 for 循环的格式,简化了迭代器的书写
* 2、此 fo r循环必须有被遍历的目标,目标只能是 Collection 或者是数组。
* 3、是 JDK1.5 之后出现的新特性
* 4、Collection<E>extends Iterable<E>:所有的单列集合都可以使用增强 for
* 5、 public interface Iterable<T>实现这个接口允许对象成为 "foreach" 语句的目标。
* 6、格式:
* for(集合/数组的数据类型 变量名: 集合名/数组名){
* sout(变量名);
* }
*/
public static void main(String[] args) {
// 01-创建集合
ArrayList<String> list = new ArrayList<>();

// 02-添加数据
list.add("abc");
list.add("def");
list.add("ghi");
list.add("jkl");

// 03-增强 for 循环遍历集合
for (String str : list) {
System.out.println(str);
}

// 04-知识拓展:增强 for 遍历数组
System.out.println("=====");
int[] arr = {1, 2, 3, 4, 5};
for(int i : arr){
System.out.println(i);
}
}

泛型

泛型的概述

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

假定我们有这样一个需求:写一个排序方法,能够对整型数组、字符串数组甚至其他任何类型的数组进行排序,该如何实现?答案是可以使用 Java 泛型。使用 Java 泛型的概念,我们可以写一个泛型方法来对一个对象数组排序。然后,调用该泛型方法来对整型数组、浮点数数组、字符串数组等进行排序。

看一张图,了解泛型:

泛型的好处

泛型的好处:
1、将运行时期的 ClassCastException,转移到了编译时期变成了编译失败。 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
55
public static void main(String[] args) {
// 01-不使用泛型案例
//listDemoFirst();

// 02-使用泛型案例
listDemoSecond();
}


/**
* 创建集合对象,使用泛型:
* 好处:1、避免了类型转换的麻烦,存储的是什么类型,取出的就是什么类型;2、把运行期异常(代码运行之后会抛出的异常),提升到了编译期(写代码的时候会报错)
* 弊端:泛型是什么类型,只能存储什么类型的数据
*/
private static void listDemoSecond() {
// 01-创建集合并添加数据
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("Tom");
arrayList.add("Lisa");
arrayList.add("Jack");
// arrayList.add(1); //直接编译不通过,报错信息:add(java.lang.String)in ArrayList cannot be applied to (int)

// 02-迭代器遍历集合
Iterator<String> it = arrayList.iterator();
while (it.hasNext()){
String string = it.next();
System.out.println(string + " --> " + string.length());
}
}


/**
* 创建集合对象,不使用泛型:
* 好处:集合不使用泛型,默认类型就是 Object 类型,可以存储任意类型的数据
* 弊端:不安全,会引发异常
*/
private static void listDemoFirst() {
// 01-创建集合并添加数据
ArrayList list = new ArrayList();
list.add("Jack");
list.add(37);

// 02-迭代器遍历集合
Iterator iterator = list.iterator();
while (iterator.hasNext()){
// 03-取出的元素是 Object 类型的
Object object = iterator.next();
System.out.println(object);

// 04-想要使用 String 类特有的方法,length 获取字符串的长度;不能使用多态 Object obj = "abc";
// 05-需要向下转型才能使用相关类方法,然而结果抛出 ClassCastException 类型转换异常,不能把 Integer 类型转换为 String 类型,这就是弊端
String str = (String)object;
System.out.println(str.length());
}
}

tips:泛型是数据类型的一部分,我们将类名与泛型合并一起看做数据类型。

泛型的定义和使用

我们在集合中会大量使用到泛型,这里来完整地学习泛型知识。泛型,用来灵活地将数据类型应用到不同的类、方法、接口当中。将数据类型作为参数进行传递。

有泛型的类

例如,API 中的 ArrayList 集合:

1
2
3
4
5
6
7
// 含有泛型的类的定义格式:修饰符 class 类名<代表泛型的变量> {  }
class ArrayList<E>{
public boolean add(E e){ }

public E get(int index){ }
....
}

实际代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 定义一个含有泛型的类,模拟 ArrayList 集合
* 泛型是一个未知的数据类型,当我们不确定什么什么数据类型的时候,可以使用泛型
* 泛型可以接收任意的数据类型,可以使用 Integer,String,Student...
* 创建对象的时候确定泛型的数据类型
*/
public class MyArrayList<E> {
private E name;

public E getName() {
return name;
}

public void setName(E name) {
this.name = name;
}
}

使用自定义的泛型类: 即什么时候确定泛型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
// 01-创建 MyArrayList 对象,不写泛型,默认为 Object 类型
MyArrayList myArrayList = new MyArrayList();
myArrayList.setName("Jack");
Object object = myArrayList.getName();
System.out.println(object);

//02-创建 MyArrayList 对象,泛型使用 Integer 类型
MyArrayList<Integer> integerMyArrayList = new MyArrayList<>();
integerMyArrayList.setName(520);
System.out.println(integerMyArrayList.getName());

// 03-创建 MyArrayList 对象,泛型使用 String 类型
MyArrayList<String> stringMyArrayList = new MyArrayList<>();
stringMyArrayList.setName("Tom");
System.out.println(stringMyArrayList.getName());
}

有泛型的方法

例如:

1
2
3
4
5
6
7
8
9
10
// 有泛型的方法的定义格式:修饰符 <代表泛型的变量> 返回值类型 方法名(参数){  }
public class MyGenericMethod {
public <MVP> void show(MVP mvp) {
System.out.println(mvp.getClass());
}

public <MVP> MVP show2(MVP mvp) {
return mvp;
}
}

具体定义代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 定义含有泛型的方法:泛型定义在方法的修饰符和返回值类型之间
* 格式: 修饰符 <泛型> 返回值类型 方法名(参数列表(使用泛型)){
* 方法体;
* }
* 含有泛型的方法,在调用方法的时候确定泛型的数据类型 ,传递什么类型的参数,泛型就是什么类
*/

// 01-定义一个含有泛型的方法
public <M> void method01(M m){
System.out.println(m);
}

// 02-定义一个含有泛型的静态方法
public static <S> void method02(S s){
System.out.println(s);
}

使用格式:调用方法时,确定泛型的类型 ,相关代码如下:

1
2
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 GenericMethod {
public static void main(String[] args) {
// 01-创建 GenericMethod 对象
GenericMethod genericMethod = new GenericMethod();

// 02-调用含有泛型的方法 method01,传递什么类型,泛型就是什么类型
genericMethod.method01(10);
genericMethod.method01("abc");
genericMethod.method01(3.14);
genericMethod.method01(true);

// 03-调用静态方法,通过类名.方法名(参数)可以直接使用
GenericMethod.method02("静态方法");
GenericMethod.method02(1.732);
}

// 01-定义一个含有泛型的方法
public <M> void method01(M m){
System.out.println(m);
}

// 02-定义一个含有泛型的静态方法
public static <S> void method02(S s){
System.out.println(s);
}
}

有泛型的接口

定义格式及示例如下:

1
2
3
4
5
6
// 定义格式:修饰符 interface接口名<代表泛型的变量> {  }
public interface MyGenericInterface<E>{
public abstract void add(E e);

public abstract E getE();
}

含有泛型的接口的具体代码如下:

1
2
3
4
// 定义含有泛型的接口
public interface GenericInterface<E> {
public abstract void method(E e);
}

泛型接口的第一种实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 含有泛型的接口,第一种使用方式:定义接口的实现类,实现接口,指定接口的泛型
* public interface Iterator<E> {
* E next();
* }
* Scanner类实现了 Iterator 接口,并指定接口的泛型为 String , 所以重写的 next 方法泛型默认就是 String
* public final class Scanner implements Iterator<String>{
* public String next() {}
* }
*/
public class GenericInterfaceImpl1 implements GenericInterface<String>{
@Override
public void method(String str) {
System.out.println(str);
}
}

泛型接口的第二种实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 含有泛型的接口第二种使用方式:接口使用什么泛型,实现类就使用什么泛型,类跟着接口走
* 就相当于定义了一个含有泛型的类,创建对象的时候确定泛型的类型
* public interface List<E>{
* boolean add(E e);
* E get(int index);
* }
* public class ArrayList<E> implements List<E>{
* public boolean add(E e) {}
* public E get(int index) {}
* }
*/
public class GenericInterfaceImpl2<E> implements GenericInterface<E> {
@Override
public void method(E e) {
System.out.println(e);
}
}

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 测试含有泛型的接口
public class Demo04GenericInterface {
public static void main(String[] args) {
// 01-创建 GenericInterfaceImpl1 对象
GenericInterfaceImpl1 gi1 = new GenericInterfaceImpl1();
gi1.method("字符串");

// 02-创建 GenericInterfaceImpl2 对象
GenericInterfaceImpl2<Integer> gi2 = new GenericInterfaceImpl2<>();
gi2.method(10);
GenericInterfaceImpl2<Double> gi3 = new GenericInterfaceImpl2<>();
gi3.method(8.8);
}
}

泛型通配符

当使用泛型类或者接口时,传递的数据中,泛型类型不确定,可以通过通配符 <?> 表示。但是一旦使用泛型的通配符后,只能使用 Object 类中的共性方法,集合中元素自身方法无法使用。

通配符基本使用

泛型的通配符:不知道使用什么类型来接收的时候,此时可以使用 ?, ? 表示未知通配符。此时只能接受数据,不能往该集合中存储数据。举个例子大家理解使用即可:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
Collection<Integer> list1 = new ArrayList<Integer>();
getElement(list1);
Collection<String> list2 = new ArrayList<String>();
getElement(list2);
}

// ? 代表可以接收任意类型
public static void getElement(Collection<?> coll){}

具体实际代码如下:

1
2
3
4
5
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 static void main(String[] args) {
/**
* 泛型的通配符: ? 代表任意的数据类型
* 使用方式: 1、不能创建对象使用;2、只能作为方法的参数使用
*/
ArrayList<Integer> list01 = new ArrayList<>();
list01.add(1);
list01.add(2);

ArrayList<String> list02 = new ArrayList<>();
list02.add("a");
list02.add("b");

printArray(list01);
printArray(list02);

}


/**
* 定义一个方法,能遍历所有类型的 ArrayList 集合
* 这时候我们不知道 ArrayList 集合使用什么数据类型 ,可以泛型的通配符 ? 来接收数据类型
* 注意: 泛型没有继承概念的
*/
private static void printArray(ArrayList<?> list) {
// 使用迭代器遍历集合
Iterator<?> iterator = list.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}

tips:泛型不存在继承关系 Collection<Object> list = new ArrayList<String>(); 这种是错误的。

通配符高级之受限泛型

之前设置泛型的时候,实际上是可以任意设置的,只要是类就可以设置。但是在 Java 的泛型中可以指定一个泛型的 上限下限

泛型的上限:
1、格式: 类型名称 < ? extends xxx类 > 对象名称 2、意义: 只能接收 xxx类型 及其子类
泛型的下限:
1、格式: 类型名称 < ? super xxx类 > 对象名称 2、意义: 只能接收 xxx类型 及其父类型

比如:现已知 Object 类,String 类,Number 类,Integer 类,其中 Number 是 Integer 的父类

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

getElement1(list1);
getElement1(list2); // 报错
getElement1(list3);
getElement1(list4); // 报错

getElement2(list1); // 报错
getElement2(list2); // 报错
getElement2(list3);
getElement2(list4);

}
// 泛型的上限:此时的泛型?,必须是 Number 类型或者 Number 类型的子类
public static void getElement1(Collection<? extends Number> coll){}

// 泛型的下限:此时的泛型?,必须是Number类型或者Number类型的父类
public static void getElement2(Collection<? super Number> coll){}

斗地主案例(单列)

案例介绍

按照斗地主的规则,完成洗牌发牌的动作。具体规则:使用 54 张牌打乱顺序,三个玩家参与游戏,三人交替摸牌,每人 17 张牌,最后三张留作底牌。

案例分析

准备牌:
牌可以设计为一个 ArrayList ,每个字符串为一张牌。 每张牌由花色数字两部分组成,我们可以使用花色集合与数字集合嵌套迭代完成每张牌的组装。 牌由 Collections 类的 shuffle 方法进行随机排序。
发牌:
将每个人以及底牌设计为 ArrayList , 将最后 3 张牌直接存放于底牌,剩余牌通过对 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
50
51
52
53
54
55
56
57
58
59
60
61
public class DouDiZhu {
// 斗地主综合案例:1、准备牌 2、洗牌 3、发牌 4、看牌
public static void main(String[] args) {
// 01-准备牌:定义一个存储 54 张牌的 ArrayList 集合,泛型使用 String
ArrayList<String> poker = new ArrayList<>();

// 02-定义两个数组,一个数组存储牌的花色,一个数组存储牌的序号
String[] colors = {"♠","♥","♣","♦"};
String[] numbers = {"2","A","K","Q","J","10","9","8","7","6","5","4","3"};

// 03-先把大王和小王存储到 poker 集合中
poker.add("大王");
poker.add("小王");

// 05-循环嵌套遍历两个数组,组装 52 张牌
for(String number : numbers){
for (String color : colors) {
// 06-把组装好的牌存储到 poker 集合中
poker.add(color + number);
}
}
// 07-洗牌:使用集合的工具类 Collections 中的方法 static void shuffle(List<?> list) 使用默认随机源对指定列表进行置换。
Collections.shuffle(poker);

// 08-发牌:定义 4 个集合,存储玩家的牌和底牌
ArrayList<String> player01 = new ArrayList<>();
ArrayList<String> player02 = new ArrayList<>();
ArrayList<String> player03 = new ArrayList<>();
ArrayList<String> diPai = new ArrayList<>();

/**
* 1、遍历 poker 集合,获取每一张牌
* 2、使用 poker 集合的索引 % 3 给 3 个玩家轮流发牌
* 剩余 3 张牌给底牌。注意:先判断底牌(i>=51),否则牌就发没了
*/
for (int i = 0; i < poker.size() ; i++) {
// 获取每一张牌
String p = poker.get(i);
// 轮流发牌
if(i >= 51){
// 给底牌发牌
diPai.add(p);
}else if(i % 3 == 0){
// 给 玩家1 发牌
player01.add(p);
}else if(i % 3 == 1){
// 给 玩家2 发牌
player02.add(p);
}else if(i % 3 == 2){
// 给 玩家3 发牌
player03.add(p);
}
}

// 09-看牌
System.out.println("刘德华:" + player01);
System.out.println("周润发:" + player02);
System.out.println("周星驰:" + player03);
System.out.println("底牌:" + diPai);
}
}

Java 数据结构

栈(stack):
1、概念:栈又称堆栈,它是运算受限的线性表,仅允许在一端进行插入和删除操作,不允许在其他任何位置进行添加、删除等操作。 2、特点:先进后出(即,存进去的元素,要在后它后面的元素依次取出后,才能取出该元素)。 3、举例:子弹压进弹夹,先压进去的子弹在下面,后压进去的子弹在上面,当开枪时,先打出上面的子弹,然后才能打出下面的子弹。 4、出入口:栈的入口、出口的都是栈的顶端位置。 5、压栈:就是存元素。即,把元素存储到栈的顶端位置,栈中已有元素依次向栈底方向移动一个位置。 6、弹栈:就是取元素。即,把栈的顶端位置元素取出,栈中已有元素依次向栈顶方向移动一个位置。

队列

队列(queue):
1、概念:队列简称队,它同堆栈一样,也是一种运算受限的线性表,其限制是仅允许在表的一端进行插入,而在表的另一端进行删除。 2、特点:先进先出(即,先存进去的元素先取出来,后存进去的元素后取出) 3、举例:例如,小火车过山洞,车头先进去,车尾后进去;车头先出来,车尾后出来。 4、出入口:队列的入口、出口各占一侧。就像管道一样,一边进一边出。

数组

数组(Array):
1、概念:是有序的元素序列,数组是在内存中开辟一段连续的空间,并在此空间存放元素。 2、特点:查找元素快(通过索引,可以快速访问指定位置的元素),增删元素慢。 3、举例:就像是一排出租屋,有100个房间,从001到100每个房间都有固定编号,通过编号就可以快速找到租房子的人。

链表

链表(linked list):
1、概念:链表由一系列结点 node(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 2、特点:查找元素慢(想查找某个元素,需要通过连接的节点,依次向后查找指定元素)。增删元素快,增加元素:只需要修改连接下个元素的地址即可。删除元素:只需要修改连接下个元素的地址即可。 3、举例:多个结点之间,通过地址进行连接。例如,多个人手拉手,每个人使用自己的右手拉住下个人的左手,依次类推,这样多个人就连在一起了。

红黑树

二叉树(binary tree):
1、二叉树概念:二叉树是每个节点最多有两个子树的树结构。顶上的叫根结点,两边被称作“左子树”和“右子树”。简单的理解,就是一种类似于我们生活中树的结构,只不过每个结点上都最多只能有两个子结点。 2、红黑树概念:红黑树本身就是一颗二叉查找树,将节点插入后,该树仍然是一颗二叉查找树。也就意味着,树的键值仍然是有序的。 2、红黑树特点:速度特别快,趋近平衡树,查找叶子元素最少和最多次数不多于二倍。
红黑树约束:
1、节点可以是红色的或者黑色的 2、根节点是黑色的 3、叶子节点(特指空节点)是黑色的 4、每个红色节点的子节点都是黑色的 5、任何一个节点到其每一个叶子节点的所有路径上黑色节点数相同

这里写的数据结构只作为了解和参考,并没有什么实际用途。此处也没有写非常详细的东西,所以不要太在意错误或者其他无法理解的地方。如果要看数据结构,很详细的数据结构,请移步: 数据结构入门

List 集合

我们掌握了 Collection 接口的使用后,再来看看 Collection 接口中的子类,他们都具备那些特性呢?接下来,我们一起学习 Collection 中的常用几个子类(java.util.List集合、java.util.Set集合)。

List接口介绍

java.util.List接口继承自Collection接口,是单列集合的一个重要分支,习惯性地会将实现了List接口的对象称为 List 集合。在 List 集合中允许出现重复的元素,所有的元素是以一种线性方式进行存储的,在程序中可以通过索引来访问集合中的指定元素。另外,List 集合还有一个特点就是元素有序,即元素的存入顺序和取出顺序一致。

看完API,我们总结一下 List 接口特点:

  • 它是一个元素存取有序的集合。例如,存元素的顺序是11、22、33。那么集合中,元素的存储就是按照11、22、33的顺序完成的)。
  • 它是一个带有索引的集合,通过索引就可以精确的操作集合中的元素(与数组的索引是一个道理)。
  • 集合中可以有重复的元素,通过元素的equals方法,来比较是否为重复的元素。

tips:我们已经学习过 List 接口的子类 java.util.ArrayList 类,该类中的方法都是来自 List 中定义。

List 中常用方法

List 作为 Collection 集合的子接口,不但继承了 Collection 接口中的全部方法,而且还增加了一些根据元素索引来操作集合的特有方法,如下:

  • public void add(int index, E element): 将指定的元素,添加到该集合中的指定位置上。
  • public E get(int index):返回集合中指定位置的元素。
  • public E remove(int index): 移除列表中指定位置的元素, 返回的是被移除的元素。
  • public E set(int index, E element):用指定元素替换集合中指定位置的元素,返回值的更新前的元素。

List集合特有的方法都是跟索引相关,我们已经都学习过,那么我们再来复习一遍吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/*
java.util.List接口 extends Collection接口
List接口的特点:
1.有序的集合,存储元素和取出元素的顺序是一致的(存储123 取出123)
2.有索引,包含了一些带索引的方法
3.允许存储重复的元素

List接口中带索引的方法(特有)
- public void add(int index, E element): 将指定的元素,添加到该集合中的指定位置上。
- public E get(int index):返回集合中指定位置的元素。
- public E remove(int index): 移除列表中指定位置的元素, 返回的是被移除的元素。
- public E set(int index, E element):用指定元素替换集合中指定位置的元素,返回值的更新前的元素。
注意:
操作索引的时候,一定要防止索引越界异常
IndexOutOfBoundsException:索引越界异常,集合会报
ArrayIndexOutOfBoundsException:数组索引越界异常
StringIndexOutOfBoundsException:字符串索引越界异常
*/
public class Demo01List {
public static void main(String[] args) {
//创建一个List集合对象,多态
List<String> list = new ArrayList<>();
//使用add方法往集合中添加元素
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("a");
//打印集合
System.out.println(list);//[a, b, c, d, a] 不是地址重写了toString

//public void add(int index, E element): 将指定的元素,添加到该集合中的指定位置上。
//在c和d之间添加一个itheima
list.add(3,"itheima");//[a, b, c, itheima, d, a]
System.out.println(list);

//public E remove(int index): 移除列表中指定位置的元素, 返回的是被移除的元素。
//移除元素
String removeE = list.remove(2);
System.out.println("被移除的元素:"+removeE);//被移除的元素:c
System.out.println(list);//[a, b, itheima, d, a]

//public E set(int index, E element):用指定元素替换集合中指定位置的元素,返回值的更新前的元素。
//把最后一个a,替换为A
String setE = list.set(4, "A");
System.out.println("被替换的元素:"+setE);//被替换的元素:a
System.out.println(list);//[a, b, itheima, d, A]

//List集合遍历有3种方式
//使用普通的for循环
for(int i=0; i<list.size(); i++){
//public E get(int index):返回集合中指定位置的元素。
String s = list.get(i);
System.out.println(s);
}
System.out.println("-----------------");
//使用迭代器
Iterator<String> it = list.iterator();
while(it.hasNext()){
String s = it.next();
System.out.println(s);
}
System.out.println("-----------------");
//使用增强for
for (String s : list) {
System.out.println(s);
}

String r = list.get(5);//IndexOutOfBoundsException: Index 5 out-of-bounds for length 5
System.out.println(r);

}
}

ArrayList 集合

java.util.ArrayList集合数据存储的结构是数组结构。元素增删慢,查找快,由于日常开发中使用最多的功能为查询数据、遍历数据,所以ArrayList是最常用的集合。许多程序员开发时非常随意地使用 ArrayList 完成任何需求,并不严谨,这种用法是不提倡的。

想要了解更多的信息,请参阅 API 或者查看他人博客。也可看先前文章:https://guoshizhan.club/JavaSE-02.html

LinkedList 集合

java.util.LinkedList集合数据存储的结构是链表结构。方便元素添加、删除的集合。

LinkedList是一个双向链表,那么双向链表是什么样子的呢,我们用个图了解下

实际开发中对一个集合元素的添加与删除经常涉及到首尾操作,而LinkedList提供了大量首尾操作的方法。这些方法我们作为了解即可:

  • public void addFirst(E e):将指定元素插入此列表的开头。
  • public void addLast(E e):将指定元素添加到此列表的结尾。
  • public E getFirst():返回此列表的第一个元素。
  • public E getLast():返回此列表的最后一个元素。
  • public E removeFirst():移除并返回此列表的第一个元素。
  • public E removeLast():移除并返回此列表的最后一个元素。
  • public E pop():从此列表所表示的堆栈处弹出一个元素。
  • public void push(E e):将元素推入此列表所表示的堆栈。
  • public boolean isEmpty():如果列表不包含元素,则返回true。

LinkedList是List的子类,List中的方法LinkedList都是可以使用,这里就不做详细介绍,我们只需要了解LinkedList的特有方法即可。在开发时,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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/*
java.util.LinkedList集合 implements List接口
LinkedList集合的特点:
1.底层是一个链表结构:查询慢,增删快
2.里边包含了大量操作首尾元素的方法
注意:使用LinkedList集合特有的方法,不能使用多态

- public void addFirst(E e):将指定元素插入此列表的开头。
- public void addLast(E e):将指定元素添加到此列表的结尾。
- public void push(E e):将元素推入此列表所表示的堆栈。

- public E getFirst():返回此列表的第一个元素。
- public E getLast():返回此列表的最后一个元素。

- public E removeFirst():移除并返回此列表的第一个元素。
- public E removeLast():移除并返回此列表的最后一个元素。
- public E pop():从此列表所表示的堆栈处弹出一个元素。

- public boolean isEmpty():如果列表不包含元素,则返回true。

*/
public class Demo02LinkedList {
public static void main(String[] args) {
show03();
}

/*
- public E removeFirst():移除并返回此列表的第一个元素。
- public E removeLast():移除并返回此列表的最后一个元素。
- public E pop():从此列表所表示的堆栈处弹出一个元素。此方法相当于 removeFirst
*/
private static void show03() {
//创建LinkedList集合对象
LinkedList<String> linked = new LinkedList<>();
//使用add方法往集合中添加元素
linked.add("a");
linked.add("b");
linked.add("c");
System.out.println(linked);//[a, b, c]

//String first = linked.removeFirst();
String first = linked.pop();
System.out.println("被移除的第一个元素:"+first);
String last = linked.removeLast();
System.out.println("被移除的最后一个元素:"+last);
System.out.println(linked);//[b]
}

/*
- public E getFirst():返回此列表的第一个元素。
- public E getLast():返回此列表的最后一个元素。
*/
private static void show02() {
//创建LinkedList集合对象
LinkedList<String> linked = new LinkedList<>();
//使用add方法往集合中添加元素
linked.add("a");
linked.add("b");
linked.add("c");

//linked.clear();//清空集合中的元素 在获取集合中的元素会抛出NoSuchElementException

//public boolean isEmpty():如果列表不包含元素,则返回true。
if(!linked.isEmpty()){
String first = linked.getFirst();
System.out.println(first);//a
String last = linked.getLast();
System.out.println(last);//c
}
}

/*
- public void addFirst(E e):将指定元素插入此列表的开头。
- public void addLast(E e):将指定元素添加到此列表的结尾。
- public void push(E e):将元素推入此列表所表示的堆栈。此方法等效于 addFirst(E)。
*/
private static void show01() {
//创建LinkedList集合对象
LinkedList<String> linked = new LinkedList<>();
//使用add方法往集合中添加元素
linked.add("a");
linked.add("b");
linked.add("c");
System.out.println(linked);//[a, b, c]

//public void addFirst(E e):将指定元素插入此列表的开头。
//linked.addFirst("www");
linked.push("www");
System.out.println(linked);//[www, a, b, c]

//public void addLast(E e):将指定元素添加到此列表的结尾。此方法等效于 add()
linked.addLast("com");
System.out.println(linked);//[www, a, b, c, com]
}
}

Vector 集合

这个集合比较老了,是从 JDK1.0 版本就有的。在此只是去知道它的存在,具体用法差不多,不懂就查 Java 编程手册。

Map 集合

Map 概述

现实生活中,我们常会看到这样的一种集合:IP 地址与主机名,身份证号与个人,系统用户名与系统用户对象等,这种一一对应的关系,就叫做映射。Java 提供了专门的集合类用来存放这种对象关系的对象,即 java.util.Map 接口。

Map 常用子类

通过查看 Map 接口描述,看到 Map 有多个子类,这里我们主要讲解常用的 HashMap 集合、LinkedHashMap 集合。

  • HashMap:存储数据采用的哈希表结构,元素的存取顺序不能保证一致。由于要保证键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。
  • LinkedHashMap:HashMap下有个子类LinkedHashMap,存储数据采用的哈希表结构+链表结构。通过链表结构可以保证元素的存取顺序一致;通过哈希表结构可以保证的键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。

tips:Map 接口中的集合都有两个泛型变量,在使用时,要为两个泛型变量赋予数据类型。两个泛型变量的数据类型可以相同,也可以不同。

Map 的常用方法

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
/*
java.util.Map<k,v>集合
Map集合的特点:
1.Map集合是一个双列集合,一个元素包含两个值(一个key,一个value)
2.Map集合中的元素,key和value的数据类型可以相同,也可以不同
3.Map集合中的元素,key是不允许重复的,value是可以重复的
4.Map集合中的元素,key和value是一一对应
java.util.HashMap<k,v>集合 implements Map<k,v>接口
HashMap集合的特点:
1.HashMap集合底层是哈希表:查询的速度特别的快
JDK1.8之前:数组+单向链表
JDK1.8之后:数组+单向链表|红黑树(链表的长度超过8):提高查询的速度
2.hashMap集合是一个无序的集合,存储元素和取出元素的顺序有可能不一致
java.util.LinkedHashMap<k,v>集合 extends HashMap<k,v>集合
LinkedHashMap的特点:
1.LinkedHashMap集合底层是哈希表+链表(保证迭代的顺序)
2.LinkedHashMap集合是一个有序的集合,存储元素和取出元素的顺序是一致的
*/
public class Demo01Map {
public static void main(String[] args) {
show04();
}

/*
boolean containsKey(Object key) 判断集合中是否包含指定的键。
包含返回true,不包含返回false
*/
private static void show04() {
//创建Map集合对象
Map<String,Integer> map = new HashMap<>();
map.put("赵丽颖",168);
map.put("杨颖",165);
map.put("林志玲",178);

boolean b1 = map.containsKey("赵丽颖");
System.out.println("b1:"+b1);//b1:true

boolean b2 = map.containsKey("赵颖");
System.out.println("b2:"+b2);//b2:false
}

/*
public V get(Object key) 根据指定的键,在Map集合中获取对应的值。
返回值:
key存在,返回对应的value值
key不存在,返回null
*/
private static void show03() {
//创建Map集合对象
Map<String,Integer> map = new HashMap<>();
map.put("赵丽颖",168);
map.put("杨颖",165);
map.put("林志玲",178);

Integer v1 = map.get("杨颖");
System.out.println("v1:"+v1);//v1:165

Integer v2 = map.get("迪丽热巴");
System.out.println("v2:"+v2);//v2:null
}

/*
public V remove(Object key): 把指定的键 所对应的键值对元素 在Map集合中删除,返回被删除元素的值。
返回值:V
key存在,v返回被删除的值
key不存在,v返回null
*/
private static void show02() {
//创建Map集合对象
Map<String,Integer> map = new HashMap<>();
map.put("赵丽颖",168);
map.put("杨颖",165);
map.put("林志玲",178);
System.out.println(map);//{林志玲=178, 赵丽颖=168, 杨颖=165}

Integer v1 = map.remove("林志玲");
System.out.println("v1:"+v1);//v1:178

System.out.println(map);//{赵丽颖=168, 杨颖=165}

//int v2 = map.remove("林志颖");//自动拆箱 NullPointerException
Integer v2 = map.remove("林志颖");
System.out.println("v2:"+v2);//v2:null

System.out.println(map);//{赵丽颖=168, 杨颖=165}
}

/*
public V put(K key, V value): 把指定的键与指定的值添加到Map集合中。
返回值:v
存储键值对的时候,key不重复,返回值V是null
存储键值对的时候,key重复,会使用新的value替换map中重复的value,返回被替换的value值
*/
private static void show01() {
//创建Map集合对象,多态
Map<String,String> map = new HashMap<>();

String v1 = map.put("李晨", "范冰冰1");
System.out.println("v1:"+v1);//v1:null

String v2 = map.put("李晨", "范冰冰2");
System.out.println("v2:"+v2);//v2:范冰冰1

System.out.println(map);//{李晨=范冰冰2}

map.put("冷锋","龙小云");
map.put("杨过","小龙女");
map.put("尹志平","小龙女");
System.out.println(map);//{杨过=小龙女, 尹志平=小龙女, 李晨=范冰冰2, 冷锋=龙小云}
}
}

tips:使用put方法时,若指定的键(key)在集合中没有,则没有这个键对应的值,返回null,并把指定的键值添加到集合中;若指定的键(key)在集合中存在,则返回值为集合中键对应的值(该值为替换前的值),并把指定键所对应的值,替换成指定的新值。

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
39
40
41
42
43
44
45
46

/*
Map集合的第一种遍历方式:通过键找值的方式
Map集合中的方法:
Set<K> keySet() 返回此映射中包含的键的 Set 视图。
实现步骤:
1.使用Map集合中的方法keySet(),把Map集合所有的key取出来,存储到一个Set集合中
2.遍历set集合,获取Map集合中的每一个key
3.通过Map集合中的方法get(key),通过key找到value
*/
public class Demo02KeySet {
public static void main(String[] args) {
//创建Map集合对象
Map<String,Integer> map = new HashMap<>();
map.put("赵丽颖",168);
map.put("杨颖",165);
map.put("林志玲",178);

//1.使用Map集合中的方法keySet(),把Map集合所有的key取出来,存储到一个Set集合中
Set<String> set = map.keySet();

//2.遍历set集合,获取Map集合中的每一个key
//使用迭代器遍历Set集合
Iterator<String> it = set.iterator();
while (it.hasNext()){
String key = it.next();
//3.通过Map集合中的方法get(key),通过key找到value
Integer value = map.get(key);
System.out.println(key+"="+value);
}
System.out.println("-------------------");
//使用增强for遍历Set集合
for(String key : set){
//3.通过Map集合中的方法get(key),通过key找到value
Integer value = map.get(key);
System.out.println(key+"="+value);
}
System.out.println("-------------------");
//使用增强for遍历Set集合
for(String key : map.keySet()){
//3.通过Map集合中的方法get(key),通过key找到value
Integer value = map.get(key);
System.out.println(key+"="+value);
}
}
}

Entry键值对对象

1
2
3
4
5
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
/*
Map集合遍历的第二种方式:使用Entry对象遍历

Map集合中的方法:
Set<Map.Entry<K,V>> entrySet() 返回此映射中包含的映射关系的 Set 视图。

实现步骤:
1.使用Map集合中的方法entrySet(),把Map集合中多个Entry对象取出来,存储到一个Set集合中
2.遍历Set集合,获取每一个Entry对象
3.使用Entry对象中的方法getKey()和getValue()获取键与值
*/
public class Demo03EntrySet {
public static void main(String[] args) {
//创建Map集合对象
Map<String,Integer> map = new HashMap<>();
map.put("赵丽颖",168);
map.put("杨颖",165);
map.put("林志玲",178);

//1.使用Map集合中的方法entrySet(),把Map集合中多个Entry对象取出来,存储到一个Set集合中
Set<Map.Entry<String, Integer>> set = map.entrySet();

//2.遍历Set集合,获取每一个Entry对象
//使用迭代器遍历Set集合
Iterator<Map.Entry<String, Integer>> it = set.iterator();
while(it.hasNext()){
Map.Entry<String, Integer> entry = it.next();
//3.使用Entry对象中的方法getKey()和getValue()获取键与值
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key+"="+value);
}
System.out.println("-----------------------");
for(Map.Entry<String,Integer> entry:set){
//3.使用Entry对象中的方法getKey()和getValue()获取键与值
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key+"="+value);
}
}
}

HashMap 存储自定义类型键值

1
2
3
4
5
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
public class Person {
private String name;
private int age;

public Person() {
}

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

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

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

@Override
public int hashCode() {

return Objects.hash(name, age);
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}
1
2
3
4
5
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
/*
HashMap存储自定义类型键值
Map集合保证key是唯一的:
作为key的元素,必须重写hashCode方法和equals方法,以保证key唯一
*/
public class Demo01HashMapSavePerson {
public static void main(String[] args) {
show02();
}

/*
HashMap存储自定义类型键值
key:Person类型
Person类就必须重写hashCode方法和equals方法,以保证key唯一
value:String类型
可以重复
*/
private static void show02() {
//创建HashMap集合
HashMap<Person,String> map = new HashMap<>();
//往集合中添加元素
map.put(new Person("女王",18),"英国");
map.put(new Person("秦始皇",18),"秦国");
map.put(new Person("普京",30),"俄罗斯");
map.put(new Person("女王",18),"毛里求斯");
//使用entrySet和增强for遍历Map集合
Set<Map.Entry<Person, String>> set = map.entrySet();
for (Map.Entry<Person, String> entry : set) {
Person key = entry.getKey();
String value = entry.getValue();
System.out.println(key+"-->"+value);
}
}

/*
HashMap存储自定义类型键值
key:String类型
String类重写hashCode方法和equals方法,可以保证key唯一
value:Person类型
value可以重复(同名同年龄的人视为同一个)
*/
private static void show01() {
//创建HashMap集合
HashMap<String,Person> map = new HashMap<>();
//往集合中添加元素
map.put("北京",new Person("张三",18));
map.put("上海",new Person("李四",19));
map.put("广州",new Person("王五",20));
map.put("北京",new Person("赵六",18));
//使用keySet加增强for遍历Map集合
Set<String> set = map.keySet();
for (String key : set) {
Person value = map.get(key);
System.out.println(key+"-->"+value);
}
}
}

LinkedHashMap

我们知道HashMap保证成对元素唯一,并且查询速度很快,可是成对元素存放进去是没有顺序的,那么我们要保证有序,还要速度快怎么办呢?在HashMap下面有一个子类LinkedHashMap,它是链表和哈希表组合的一个数据存储结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
java.util.LinkedHashMap<K,V> entends HashMap<K,V>
Map 接口的哈希表和链接列表实现,具有可预知的迭代顺序。
底层原理:
哈希表+链表(记录元素的顺序)
*/
public class Demo01LinkedHashMap {
public static void main(String[] args) {
HashMap<String,String> map = new HashMap<>();
map.put("a","a");
map.put("c","c");
map.put("b","b");
map.put("a","d");
System.out.println(map);// key不允许重复,无序 {a=d, b=b, c=c}

LinkedHashMap<String,String> linked = new LinkedHashMap<>();
linked.put("a","a");
linked.put("c","c");
linked.put("b","b");
linked.put("a","d");
System.out.println(linked);// key不允许重复,有序 {a=d, c=c, b=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
/*
java.util.Hashtable<K,V>集合 implements Map<K,V>接口

Hashtable:底层也是一个哈希表,是一个线程安全的集合,是单线程集合,速度慢
HashMap:底层是一个哈希表,是一个线程不安全的集合,是多线程的集合,速度快

HashMap集合(之前学的所有的集合):可以存储null值,null键
Hashtable集合,不能存储null值,null键

Hashtable和Vector集合一样,在jdk1.2版本之后被更先进的集合(HashMap,ArrayList)取代了
Hashtable的子类Properties依然活跃在历史舞台
Properties集合是一个唯一和IO流相结合的集合
*/
public class Demo02Hashtable {
public static void main(String[] args) {
HashMap<String,String> map = new HashMap<>();
map.put(null,"a");
map.put("b",null);
map.put(null,null);
System.out.println(map);//{null=null, b=null}

Hashtable<String,String> table = new Hashtable<>();
//table.put(null,"a");//NullPointerException
//table.put("b",null);//NullPointerException
table.put(null,null);//NullPointerException
}
}
1
2


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
39
40
41
42
43
44
45
/*
练习:
计算一个字符串中每个字符出现次数

分析:
1.使用Scanner获取用户输入的字符串
2.创建Map集合,key是字符串中的字符,value是字符的个数
3.遍历字符串,获取每一个字符
4.使用获取到的字符,去Map集合判断key是否存在
key存在:
通过字符(key),获取value(字符个数)
value++
put(key,value)把新的value存储到Map集合中
key不存在:
put(key,1)
5.遍历Map集合,输出结果
*/
public class Demo03MapTest {
public static void main(String[] args) {
//1.使用Scanner获取用户输入的字符串
Scanner sc = new Scanner(System.in);
System.out.println("请输入一个字符串:");
String str = sc.next();
//2.创建Map集合,key是字符串中的字符,value是字符的个数
HashMap<Character,Integer> map = new HashMap<>();
//3.遍历字符串,获取每一个字符
for(char c :str.toCharArray()){
//4.使用获取到的字符,去Map集合判断key是否存在
if(map.containsKey(c)){
//key存在
Integer value = map.get(c);
value++;
map.put(c,value);
}else{
//key不存在
map.put(c,1);
}
}
//5.遍历Map集合,输出结果
for(Character key :map.keySet()){
Integer value = map.get(key);
System.out.println(key+"="+value);
}
}
}

JDK9 新特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*
JDK9的新特性:
List接口,Set接口,Map接口:里边增加了一个静态的方法of,可以给集合一次性添加多个元素
static <E> List<E> of​(E... elements)
使用前提:
当集合中存储的元素的个数已经确定了,不在改变时使用
注意:
1.of方法只适用于List接口,Set接口,Map接口,不适用于接接口的实现类
2.of方法的返回值是一个不能改变的集合,集合不能再使用add,put方法添加元素,会抛出异常
3.Set接口和Map接口在调用of方法的时候,不能有重复的元素,否则会抛出异常
*/
public class Demo01JDK9 {
public static void main(String[] args) {
List<String> list = List.of("a", "b", "a", "c", "d");
System.out.println(list);//[a, b, a, c, d]
//list.add("w");//UnsupportedOperationException:不支持操作异常

//Set<String> set = Set.of("a", "b", "a", "c", "d");//IllegalArgumentException:非法参数异常,有重复的元素
Set<String> set = Set.of("a", "b", "c", "d");
System.out.println(set);
//set.add("w");//UnsupportedOperationException:不支持操作异常

//Map<String, Integer> map = Map.of("张三", 18, "李四", 19, "王五", 20,"张三",19);////IllegalArgumentException:非法参数异常,有重复的元素
Map<String, Integer> map = Map.of("张三", 18, "李四", 19, "王五", 20);
System.out.println(map);//{王五=20, 李四=19, 张三=18}
//map.put("赵四",30);//UnsupportedOperationException:不支持操作异常
}
}

Debug 调试

1
2
3
4
5
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
/*
Debug调试程序:
可以让代码逐行执行,查看代码执行的过程,调试程序中出现的bug
使用方式:
在行号的右边,鼠标左键单击,添加断点(每个方法的第一行,哪里有bug添加到哪里)
右键,选择Debug执行程序
程序就会停留在添加的第一个断点处
执行程序:
f8:逐行执行程序
f7:进入到方法中
shift+f8:跳出方法
f9:跳到下一个断点,如果没有下一个断点,那么就结束程序
ctrl+f2:退出debug模式,停止程序
Console:切换到控制台
*/
public class Demo01Debug {
public static void main(String[] args) {
/*int a = 10;
int b = 20;
int sum = a + b;
System.out.println(sum);*/

/*for (int i = 0; i <3 ; i++) {
System.out.println(i);
}*/

print();

}

private static void print() {
System.out.println("HelloWorld");
System.out.println("HelloWorld");
System.out.println("HelloWorld");
System.out.println("HelloWorld");
System.out.println("HelloWorld");
}
}

斗地主案例(双列)

案例介绍

按照斗地主的规则,完成洗牌发牌的动作。具体规则如下:

  • 组装 54 张扑克牌将
  • 54 张牌顺序打乱
  • 三个玩家参与游戏,三人交替摸牌,每人 17 张牌,最后三张留作底牌。
  • 查看三人各自手中的牌(按照牌的大小排序)、底牌

规则:手中扑克牌从大到小的摆放顺序:大王,小王,2,A,K,Q,J,10,9,8,7,6,5,4,3

案例分析

  • 准备牌:
    完成数字与纸牌的映射关系:使用双列Map(HashMap)集合,完成一个数字与字符串纸牌的对应关系(相当于一个字典)。

  • 洗牌:
    通过数字完成洗牌发牌

  • 发牌:
    将每个人以及底牌设计为ArrayList,将最后3张牌直接存放于底牌,剩余牌通过对3取模依次发牌。存放的过程中要求数字大小与斗地主规则的大小对应。将代表不同纸牌的数字分配给不同的玩家与底牌。

  • 看牌:
    通过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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
/*
斗地主综合案例:有序版本
1.准备牌
2.洗牌
3.发牌
4.排序
5.看牌
*/
public class DouDiZhu {
public static void main(String[] args) {
//1.准备牌
//创建一个Map集合,存储牌的索引和组装好的牌
HashMap<Integer,String> poker = new HashMap<>();
//创建一个List集合,存储牌的索引
ArrayList<Integer> pokerIndex = new ArrayList<>();
//定义两个集合,存储花色和牌的序号
List<String> colors = List.of("♠", "♥", "♣", "♦");
List<String> numbers = List.of("2", "A", "K", "Q", "J", "10", "9", "8", "7", "6", "5", "4", "3");
//把大王和小王存储到集合中
//定义一个牌的索引
int index = 0;
poker.put(index,"大王");
pokerIndex.add(index);
index++;
poker.put(index,"小王");
pokerIndex.add(index);
index++;
//循环嵌套遍历两个集合,组装52张牌,存储到集合中
for (String number : numbers) {
for (String color : colors) {
poker.put(index,color+number);
pokerIndex.add(index);
index++;
}
}
//System.out.println(poker);
//System.out.println(pokerIndex);

/*
2.洗牌
使用Collections中的方法shuffle(List)
*/
Collections.shuffle(pokerIndex);
//System.out.println(pokerIndex);

/*
3.发牌
*/
//定义4个集合,存储玩家牌的索引,和底牌的索引
ArrayList<Integer> player01 = new ArrayList<>();
ArrayList<Integer> player02 = new ArrayList<>();
ArrayList<Integer> player03 = new ArrayList<>();
ArrayList<Integer> diPai = new ArrayList<>();
//遍历存储牌索引的List集合,获取每一个牌的索引
for (int i = 0; i <pokerIndex.size() ; i++) {
Integer in = pokerIndex.get(i);
//先判断底牌
if(i>=51){
//给底牌发牌
diPai.add(in);
}else if(i%3==0){
//给玩家1发牌
player01.add(in);
}else if(i%3==1){
//给玩家2发牌
player02.add(in);
}else if(i%3==2){
//给玩家3发牌
player03.add(in);
}
}

/*
4.排序
使用Collections中的方法sort(List)
默认是升序排序
*/
Collections.sort(player01);
Collections.sort(player02);
Collections.sort(player03);
Collections.sort(diPai);

/*
5.看牌
调用看牌的方法
*/
lookPoker("刘德华",poker,player01);
lookPoker("周润发",poker,player02);
lookPoker("周星驰",poker,player03);
lookPoker("底牌",poker,diPai);
}

/*
定义一个看牌的方法,提高代码的复用性
参数:
String name:玩家名称
HashMap<Integer,String> poker:存储牌的poker集合
ArrayList<Integer> list:存储玩家和底牌的List集合
查表法:
遍历玩家或者底牌集合,获取牌的索引
使用牌的索引,去Map集合中,找到对应的牌
*/
public static void lookPoker(String name,HashMap<Integer,String> poker,ArrayList<Integer> list){
//输出玩家名称,不换行
System.out.print(name+":");
//遍历玩家或者底牌集合,获取牌的索引
for (Integer key : list) {
//使用牌的索引,去Map集合中,找到对应的牌
String value = poker.get(key);
System.out.print(value+" ");
}
System.out.println();//打印完每一个玩家的牌,换行
}

}

异常处理

异常概念

异常 :就是不正常的意思,指的是程序在执行过程中,出现的非正常的情况,最终会导致 JVM 的非正常停止。在 Java 等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出了一个异常对象。Java 处理异常的方式是中断处理。

异常指的并不是语法错误,语法错了,编译不通过,不会产生字节码文件,根本不能运行。

异常体系

异常机制其实是帮助我们找到程序中的问题,异常的根类是 java.lang.Throwable ,其下有两个子类:java.lang.Errorjava.lang.Exception ,平常所说的异常指 java.lang.Exception 。如下图:

Throwable 常用方法

以下三个方法都是为了获取异常信息:

public void printStackTrace() : 打印异常的详细信息。包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用 printStackTrace。
public String getMessage():获取发生异常的原因。提示给用户的时候,就提示错误原因。
public String toString():获取异常的类型和异常描述信息(不用)。

异常分类

编译时期异常 : checked 异常。在编译时期,就会检查,如果没有处理异常,则编译失败。(如日期格式化异常)
运行时期异常 : runtime 异常。在运行时期,检查异常.在编译时期,运行异常不会编译器检测(不报错)。(如数学异常)

异常处理

Java异常处理的五个关键字:try、catch、finally、throw、throws

抛出异常 throw

在编写程序时,我们必须要考虑程序出现问题的情况。比如,在定义方法时,方法需要接受参数。那么,当调用方法使用接受到的参数时,首先需要先对参数数据进行合法的判断,数据若不合法,就应该告诉调用者,传递合法的数据进来。这时需要使用抛出异常的方式来告诉调用者。在 java 中,提供了一个 throw 关键字,它用来抛出一个指定的异常对象。那么,抛出一个异常具体如何操作呢?

  • 1、创建一个异常对象。封装一些提示信息(信息可以自己编写)。

  • 2、需要将这个异常对象告知给调用者。怎么告知呢?怎么将这个异常对象传递到调用者处呢?通过关键字 throw 就可以完成。throw 异常对象。throw 用在方法内 ,用来抛出一个异常对象,将这个异常对象传递到调用者处,并结束当前方法的执行。

使用格式:

1
throw new 异常类名(参数);

例如:

1
2
3
throw new NullPointerException("要访问的arr数组不存在");

throw new ArrayIndexOutOfBoundsException("该索引在数组中不存在,已超出范围");

学习完抛出异常的格式后,我们通过下面程序演示下 throw 的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ThrowDemo {
public static void main(String[] args) {
// 创建一个数组
int[] arr = {2,4,52,2};
// 根据索引找对应的元素
int index = 4;
int element = getElement(arr, index);
System.out.println(element);
System.out.println("over");
}

// 根据 索引找到数组中对应的元素
public static int getElement(int[] arr,int index){
// 判断索引是否越界
if(index<0 || index>arr.length-1){
// 判断条件如果满足,当执行完 throw 抛出异常对象后,方法已经无法继续运算。这时就会结束当前方法的执行,并将异常告知给调用者。这时就需要通过异常来解决。
throw new ArrayIndexOutOfBoundsException("哥们,角标越界了~~~");
}
int element = arr[index];
return element;
}
}

注意:如果产生了问题,我们就会 throw 将问题描述类即异常进行抛出,也就是将问题返回给该方法的调用者。

那么对于调用者来说,该怎么处理呢?一种是进行捕获处理,另一种就是继续讲问题声明出去,使用 throws 声明处理。

声明异常 throws

声明异常:将问题标识出来,报告给调用者。如果方法内通过 throw 抛出了编译时异常,而没有捕获处理(下面讲解该方式),那么必须通过 throws 进行声明,让调用者去处理。关键字 throws 运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常(抛出异常).

声明异常格式:

1
修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2…{   }

声明异常的代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ThrowsDemo {
public static void main(String[] args) throws FileNotFoundException {
read("a.txt");
}

// 如果定义功能时有问题发生需要报告给调用者。可以通过在方法上使用throws关键字进行声明
public static void read(String path) throws FileNotFoundException {
if (!path.equals("a.txt")) {//如果不是 a.txt这个文件
// 我假设 如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常 throw
throw new FileNotFoundException("文件不存在");
}
}
}

throws用于进行异常类的声明,若该方法可能有多种异常情况产生,那么在throws后面可以写多个异常类,用逗号隔开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ThrowsDemo2 {
public static void main(String[] args) throws IOException {
read("a.txt");
}

public static void read(String path)throws FileNotFoundException, IOException {
if (!path.equals("a.txt")) { // 如果不是 a.txt这个文件
// 如果不是 a.txt ,认为该文件不存在,是一个异常 throw
throw new FileNotFoundException("文件不存在");
}
if (!path.equals("b.txt")) {
throw new IOException();
}
}
}

捕获异常 try_catch

如果异常出现的话,会立刻终止程序,所以我们得处理异常:

  • 1、该方法不处理,而是声明抛出,由该方法的调用者来处理(throws)。
  • 2、如果要处理,那就在方法中使用 try-catch 的语句块来捕获异常。

try-catch 的方式就是捕获异常【Java 中对异常有针对性的语句进行捕获,可以对出现的异常进行指定方式的处理】,语法如下:

1
2
3
4
5
6
try{
编写可能会出现异常的代码
}catch(异常类型 e){
处理异常的代码
// 记录日志/打印异常信息/继续抛出异常
}

try:在该代码块中,编写可能产生异常的代码。
catch:用来进行某种异常的捕获,实现对捕获到的异常进行处理。
注意 : try 和 catch 都不能单独使用,必须连用。

演示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TryCatchDemo {
public static void main(String[] args) {
try { // 当产生异常时,必须有处理方式。要么捕获,要么声明。
read("b.txt");
} catch (FileNotFoundException e) { // 括号中需要定义什么呢?
// try 中抛出的是什么异常,在括号中就定义什么异常类型
System.out.println(e);
}
System.out.println("over");
}
// 当前的这个方法中有异常 有编译期异常
public static void read(String path) throws FileNotFoundException {
if (!path.equals("a.txt")) {
throw new FileNotFoundException("文件不存在");
}
}
}

finally 代码块

finally :有一些特定的代码无论异常是否发生,都需要执行。另外,因为异常会引发程序跳转,导致有些语句执行不到。而 finally 就是解决这个问题的,在 finally 代码块中存放的代码都是一定会被执行的。

什么时候的代码必须最终执行?当我们在 try 语句块中打开了一些物理资源(磁盘文件/网络连接/数据库连接等),我们都得在使用完之后,最终关闭打开的资源。

finally 的语法 : try…catch….finally:自身需要处理异常,最终还得关闭资源。

finally 代码参考如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TryCatchDemo4 {
public static void main(String[] args) {
try {
read("a.txt");
} catch (FileNotFoundException e) {
// 抓取到的是编译期异常 抛出去的是运行期
throw new RuntimeException(e);
} finally {
System.out.println("不管程序怎样,这里都将会被执行。");
}
System.out.println("over");
}

public static void read(String path) throws FileNotFoundException {
if (!path.equals("a.txt")) {
throw new FileNotFoundException("文件不存在");
}
}
}

注意事项:
1、 finally 不能单独使用。
2、 当只有在 try 或者 catch 中调用退出 JVM 的相关方法,此时 finally 才不会执行,否则 finally 永远会执行。

异常注意事项

  • 多个异常使用捕获又该如何处理呢?

    1. 多个异常分别处理。
    2. 多个异常一次捕获,多次处理。
    3. 多个异常一次捕获一次处理。

一般我们是使用一次捕获多次处理方式,格式如下:

1
2
3
4
5
6
7
8
9
try{
// 编写可能会出现异常的代码
}catch(异常类型A e){ // 当try中出现A类型异常,就用该catch来捕获.
// 处理异常的代码
// 记录日志/打印异常信息/继续抛出异常
}catch(异常类型B e){ // 当try中出现B类型异常,就用该catch来捕获.
处理异常的代码
// 记录日志/打印异常信息/继续抛出异常
}

注意事项:

1、这种异常处理方式,要求多个 catch 中的异常不能相同,并且若 catch 中的多个异常之间有子父类异常的关系,那么子类异常要求在上面的 catch 处理,父类异常在下面的 catch 处理。
2、运行时异常被抛出可以不处理。即不捕获也不声明抛出。
3、如果 finally 有 return 语句,永远返回 finally 中的结果,避免该情况。
4、如果父类抛出了多个异常,子类重写父类方法时,抛出和父类相同的异常或者是父类异常的子类或者不抛出异常。
5、父类方法没有抛出异常,子类重写父类该方法时也不可抛出异常。此时子类产生该异常,只能捕获处理,不能声明抛出。

自定义异常

自定义异常概述

为什么需要自定义异常类:

我们说了Java中不同的异常类,分别表示着某一种具体的异常情况,那么在开发中总是有些异常情况是SUN没有定义好的,此时我们根据自己业务的异常情况来定义异常类。例如年龄负数问题,考试成绩负数问题等等。

在上述代码中,发现这些异常都是JDK内部定义好的,但是实际开发中也会出现很多异常,这些异常很可能在JDK中没有定义过,例如年龄负数问题,考试成绩负数问题.那么能不能自己定义异常呢?

什么是自定义异常类:

在开发中根据自己业务的异常情况来定义异常类.

自定义一个业务逻辑异常: RegisterException。一个注册异常类。

异常类如何定义:

  1. 自定义一个编译期异常: 自定义类并继承于 java.lang.Exception
  2. 自定义一个运行时期的异常类:自定义类 并继承于 java.lang.RuntimeException

自定义异常的练习

要求:我们模拟注册操作,如果用户名已存在,则抛出异常并提示:亲,该用户名已经被注册。

首先定义一个登陆异常类 RegisterException:

1
2
3
4
5
6
7
8
9
10
11
// 业务逻辑异常
public class RegisterException extends Exception {
// 空参构造
public RegisterException() {
}

// @param message 表示异常提示
public RegisterException(String message) {
super(message);
}
}

模拟登陆操作,使用数组模拟数据库中存储的数据,并提供当前注册账号是否存在方法用于判断。

1
2
3
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 Demo {
// 模拟数据库中已存在账号
private static String[] names = {"bill","hill","jill"};

public static void main(String[] args) {
// 调用方法
try{
// 可能出现异常的代码
checkUsername("nill");
System.out.println("注册成功");//如果没有异常就是注册成功
}catch(RegisterException e){
// 处理异常
e.printStackTrace();
}
}

// 判断当前注册账号是否存在
// 因为是编译期异常,又想调用者去处理 所以声明该异常
public static boolean checkUsername(String uname) throws LoginException{
for (String name : names) {
if(name.equals(uname)){ // 如果名字在这里面 就抛出登陆异常
throw new RegisterException("亲" + name + "已经被注册了!");
}
}
return true;
}
}

多线程

了解概念

并发和并行

并发:指两个或多个事件在同一个时间段内发生。例如:一个手机同时玩王者和吃鸡,只能交替进行。
并行:指两个或多个事件在同一时刻发生(同时发生)。例如:边打游戏边听歌,同时进行。

在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。

而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核 越多,并行处理的程序越多,能大大的提高电脑运行的效率。

注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。

线程与进程

  • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

  • 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

    简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

线程调度:

  • 分时调度

    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

  • 抢占式调度

    优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

    • 设置线程的优先级

    • 抢占式调度详解

      大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。此时,这些程序是在同时运行,”感觉这些软件好像在同一时刻运行着“。

      实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。
      其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

多线程原理

常用方法

设置线程的名称

多线程休眠方法有点意思,每隔一秒执行,打印一分钟

Runnable接口创建线程对象

Runnable接口创建线程对象和thread创建对象的区别

线程安全

买票重复,出现-1

解决线程安全的三种方法

线程池
线程池介绍
涉及到一个概念,面向接口编程,就是返回对象是接口类型,不用管接口的实现类

concurrent executors

Lambda 表达式

函数式编程思想概述

在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿什么东西做什么事情”。相对而言,面向对象过分强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么,而不是以什么形式做

面向对象的思想:
做一件事情,找一个能解决这个事情的对象,调用对象的方法,完成事情.

函数式编程思想:
只要能获取到结果,谁去做的,怎么做的都不重要,重视的是结果,不重视过程

冗余的 Runnable 代码

传统写法

当需要启动一个线程去完成任务时,通常会通过java.lang.Runnable接口来定义任务内容,并使用java.lang.Thread类来启动该线程。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class Demo01Runnable {
public static void main(String[] args) {
// 匿名内部类
Runnable task = new Runnable() {
@Override
public void run() { // 覆盖重写抽象方法
System.out.println("多线程任务执行!");
}
};
new Thread(task).start(); // 启动线程
}
}

本着“一切皆对象”的思想,这种做法是无可厚非的:首先创建一个Runnable接口的匿名内部类对象来指定任务内容,再将其交给一个线程来启动。

代码分析

对于Runnable的匿名内部类用法,可以分析出几点内容:

  • Thread类需要Runnable接口作为参数,其中的抽象run方法是用来指定线程任务内容的核心;
  • 为了指定run的方法体,不得不需要Runnable接口的实现类;
  • 为了省去定义一个RunnableImpl实现类的麻烦,不得不使用匿名内部类;
  • 必须覆盖重写抽象run方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错;
  • 而实际上,似乎只有方法体才是关键所在

编程思想转换

做什么,而不是怎么做

我们真的希望创建一个匿名内部类对象吗?不。我们只是为了做这件事情而不得不创建一个对象。我们真正希望做的事情是:将run方法体内的代码传递给Thread类知晓。

传递一段代码——这才是我们真正的目的。而创建对象只是受限于面向对象语法而不得不采取的一种手段方式。那,有没有更加简单的办法?如果我们将关注点从“怎么做”回归到“做什么”的本质上,就会发现只要能够更好地达到目的,过程与形式其实并不重要。

生活举例

当我们需要从北京到上海时,可以选择高铁、汽车、骑行或是徒步。我们的真正目的是到达上海,而如何才能到达上海的形式并不重要,所以我们一直在探索有没有比高铁更好的方式——搭乘飞机。

而现在这种飞机(甚至是飞船)已经诞生:2014年3月Oracle所发布的Java 8(JDK 1.8)中,加入了Lambda表达式的重量级新特性,为我们打开了新世界的大门。

体验Lambda的更优写法

借助Java 8的全新语法,上述Runnable接口的匿名内部类写法可以通过更简单的Lambda表达式达到等效:

1
2
3
4
5
public class Demo02LambdaRunnable {
public static void main(String[] args) {
new Thread(() -> System.out.println("多线程任务执行!")).start(); // 启动线程
}
}

这段代码和刚才的执行效果是完全一样的,可以在1.8或更高的编译级别下通过。从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。

不再有“不得不创建接口对象”的束缚,不再有“抽象方法覆盖重写”的负担,就是这么简单!

回顾匿名内部类

Lambda是怎样击败面向对象的?在上例中,核心代码其实只是如下所示的内容:

1
() -> System.out.println("多线程任务执行!")

为了理解Lambda的语义,我们需要从传统的代码起步。

使用实现类

要启动一个线程,需要创建一个Thread类的对象并调用start方法。而为了指定线程执行的内容,需要调用Thread类的构造方法:

  • public Thread(Runnable target)

为了获取Runnable接口的实现对象,可以为该接口定义一个实现类RunnableImpl

1
2
3
4
5
6
public class RunnableImpl implements Runnable {
@Override
public void run() {
System.out.println("多线程任务执行!");
}
}

然后创建该实现类的对象作为Thread类的构造参数:

1
2
3
4
5
6
public class Demo03ThreadInitParam {
public static void main(String[] args) {
Runnable task = new RunnableImpl();
new Thread(task).start();
}
}

使用匿名内部类

这个RunnableImpl类只是为了实现Runnable接口而存在的,而且仅被使用了唯一一次,所以使用匿名内部类的语法即可省去该类的单独定义,即匿名内部类:

1
2
3
4
5
6
7
8
9
10
public class Demo04ThreadNameless {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("多线程任务执行!");
}
}).start();
}
}

匿名内部类的好处与弊端

一方面,匿名内部类可以帮我们省去实现类的定义;另一方面,匿名内部类的语法——确实太复杂了!

语义分析

仔细分析该代码中的语义,Runnable接口只有一个run方法的定义:

  • public abstract void run();

即制定了一种做事情的方案(其实就是一个函数):

  • 无参数:不需要任何条件即可执行该方案。
  • 无返回值:该方案不产生任何结果。
  • 代码块(方法体):该方案的具体执行步骤。

同样的语义体现在Lambda语法中,要更加简单:

1
() -> System.out.println("多线程任务执行!")
  • 前面的一对小括号即run方法的参数(无),代表不需要任何条件;
  • 中间的一个箭头代表将前面的参数传递给后面的代码;
  • 后面的输出语句即业务逻辑代码。

Lambda标准格式

Lambda省去面向对象的条条框框,格式由3个部分组成:

  • 一些参数
  • 一个箭头
  • 一段代码

Lambda表达式的标准格式为:

1
(参数类型 参数名称) -> { 代码语句 }

格式说明:

  • 小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。
  • ->是新引入的语法格式,代表指向动作。
  • 大括号内的语法与传统方法体要求基本一致。

练习:使用Lambda标准格式(无参无返回)

题目

给定一个厨子Cook接口,内含唯一的抽象方法makeFood,且无参数、无返回值。如下:

1
2
3
public interface Cook {
void makeFood();
}

在下面的代码中,请使用Lambda的标准格式调用invokeCook方法,打印输出“吃饭啦!”字样:

1
2
3
4
5
6
7
8
9
public class Demo05InvokeCook {
public static void main(String[] args) {
// TODO 请在此使用Lambda【标准格式】调用invokeCook方法
}

private static void invokeCook(Cook cook) {
cook.makeFood();
}
}

解答

1
2
3
4
5
public static void main(String[] args) {
invokeCook(() -> {
System.out.println("吃饭啦!");
});
}

备注:小括号代表Cook接口makeFood抽象方法的参数为空,大括号代表makeFood的方法体。

Lambda的参数和返回值

1
2
3
需求:
使用数组存储多个Person对象
对数组中的Person对象使用Arrays的sort方法通过年龄进行升序排序

下面举例演示java.util.Comparator<T>接口的使用场景代码,其中的抽象方法定义为:

  • public abstract int compare(T o1, T o2);

当需要对一个对象数组进行排序时,Arrays.sort方法需要一个Comparator接口实例来指定排序的规则。假设有一个Person类,含有String nameint age两个成员变量:

1
2
3
4
5
6
public class Person { 
private String name;
private int age;

// 省略构造器、toString方法与Getter Setter
}

传统写法

如果使用传统的代码对Person[]数组进行排序,写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.Arrays;
import java.util.Comparator;

public class Demo06Comparator {
public static void main(String[] args) {
// 本来年龄乱序的对象数组
Person[] array = {
new Person("古力娜扎", 19),
new Person("迪丽热巴", 18),
new Person("马尔扎哈", 20) };

// 匿名内部类
Comparator<Person> comp = new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.getAge() - o2.getAge();
}
};
Arrays.sort(array, comp); // 第二个参数为排序规则,即Comparator接口实例

for (Person person : array) {
System.out.println(person);
}
}
}

这种做法在面向对象的思想中,似乎也是“理所当然”的。其中Comparator接口的实例(使用了匿名内部类)代表了“按照年龄从小到大”的排序规则。

代码分析

下面我们来搞清楚上述代码真正要做什么事情。

  • 为了排序,Arrays.sort方法需要排序规则,即Comparator接口的实例,抽象方法compare是关键;
  • 为了指定compare的方法体,不得不需要Comparator接口的实现类;
  • 为了省去定义一个ComparatorImpl实现类的麻烦,不得不使用匿名内部类;
  • 必须覆盖重写抽象compare方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错;
  • 实际上,只有参数和方法体才是关键

Lambda写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.Arrays;

public class Demo07ComparatorLambda {
public static void main(String[] args) {
Person[] array = {
new Person("古力娜扎", 19),
new Person("迪丽热巴", 18),
new Person("马尔扎哈", 20) };

Arrays.sort(array, (Person a, Person b) -> {
return a.getAge() - b.getAge();
});

for (Person person : array) {
System.out.println(person);
}
}
}

练习:使用Lambda标准格式(有参有返回)

题目

给定一个计算器Calculator接口,内含抽象方法calc可以将两个int数字相加得到和值:

1
2
3
public interface Calculator {
int calc(int a, int b);
}

在下面的代码中,请使用Lambda的标准格式调用invokeCalc方法,完成120和130的相加计算:

1
2
3
4
5
6
7
8
9
10
public class Demo08InvokeCalc {
public static void main(String[] args) {
// TODO 请在此使用Lambda【标准格式】调用invokeCalc方法来计算120+130的结果ß
}

private static void invokeCalc(int a, int b, Calculator calculator) {
int result = calculator.calc(a, b);
System.out.println("结果是:" + result);
}
}

解答

1
2
3
4
5
public static void main(String[] args) {
invokeCalc(120, 130, (int a, int b) -> {
return a + b;
});
}

备注:小括号代表 Calculator 接口 calc 抽象方法的参数,大括号代表calc的方法体。

Lambda 省略格式

可推导即可省略

Lambda强调的是“做什么”而不是“怎么做”,所以凡是可以根据上下文推导得知的信息,都可以省略。例如上例还可以使用Lambda的省略写法:

1
2
3
public static void main(String[] args) {
invokeCalc(120, 130, (a, b) -> a + b);
}

省略规则

在Lambda标准格式的基础上,使用省略写法的规则为:

  1. 小括号内参数的类型可以省略;
  2. 如果小括号内有且仅有一个参,则小括号可以省略;
  3. 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号。

备注:掌握这些省略规则后,请对应地回顾本章开头的多线程案例。

练习:使用 Lambda 省略格式

题目

仍然使用前文含有唯一 makeFood 抽象方法的厨子 Cook 接口,在下面的代码中,请使用 Lambda 的省略格式调用 invokeCook 方法,打印输出“吃饭啦!”字样:

1
2
3
4
5
6
7
8
9
public class Demo09InvokeCook {
public static void main(String[] args) {
// TODO 请在此使用Lambda【省略格式】调用invokeCook方法
}

private static void invokeCook(Cook cook) {
cook.makeFood();
}
}

解答

1
2
3
public static void main(String[] args) {
invokeCook(() -> System.out.println("吃饭啦!"));
}

Lambda 的使用前提

Lambda 的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:

  1. 使用 Lambda 必须具有接口,且要求接口中有且仅有一个抽象方法
    无论是 JDK 内置的 RunnableComparator 接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用 Lambda。
  2. 使用 Lambda 必须具有上下文推断
    也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。

备注:有且仅有一个抽象方法的接口,称为“函数式接口”。

Java 中的 IO 流

IO 流知识
由于本篇过长,IO 流独立成篇:Java 中的 IO 流
    /  / 

📚 本站推荐文章
  👉 从 0 开始搭建 Hexo 博客
  👉 计算机网络入门教程
  👉 数据结构入门
  👉 算法入门
  👉 IDEA 入门教程

可在评论区留言哦

一言句子获取中...

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×