目录
一、Arrays类,JDK8新特性:Lambda表达式、方法引用
一、Arrays类,JDK8新特性:Lambda表达式、方法引用
1、Arrays类
它是一个用来操作数组的工具类。
setAll方法是将原数组中的内容取出来,计算后,再存在在原数组中。
思考:如果数组中存储的是对象,如何排序呢?
如果就想要让Arrays进行对对象进行排序(实质是比较),此时就需要自定义比较规则,如何定义比较规则呢?java提供两种方式进行比较规则的制定:
方式一:
默认情况下调用Arrays.sort()方法会调用compareTo方法。compareTo方法就是我们设计的比较规则。
Arrays.sort()默认只会按照比较的大小进行升序排序,因此我们再进行比较时告诉sort相反的结果sort方法就降序排序了。
方式二:
Comparator是一个接口,要获得接口的对象只有两种方式,要么使用它的实现类对象,要门使用匿名内部类构造对象,而且此时需要将构建的对象当作参数传给方法,优先匿名内部类。
2、Lambda表达式
(1)认识Lambda表达式
Lambda表达式是JDK8开始新增的一种语法形式;作用是用于简化匿名内部类的代码写法。
格式如下:
使用Lambda表达式简化匿名内部类,这个匿名内部类必须是函数式接口:
注意:将来我们见到的大部分函数式接口,上面都可能会有一个@Functionalterface的注解,有该注解的接口一定是函数式接口。
(2)Lambda表达式的省略规则
上述Lambda表达式其实还可以进一步的简化,省略规则如下:
例如上述IntToDoubleFunction的简化:
上述Comparator可以简化为:
3、方法引用
方法引用还可以进一步简化Lambda表达式,方法引用的标志性符号是“::”。方法引用简化Lambda表达有四种常见的场景:静态方法引用、实例方法引用、特定类型方法引用、构造器引用。
(1)静态方法引用
如果某个Lambda表达式里只是调用一个静态方法,且前后参数的形式一致,就可以使用静态方法引用:类名::静态方法名
接下来继续对上述Lambda表达式进行简化,我们重新定义一个类,再该类中定义一个静态方法用于封装上述的方法体:o1.getAge() - o2.getAge()
此时Lambda表达式可以改写为:
此时在Lambda表达式中就是只调用一个静态方法,且前后参数的形式一致,因此Lambda表达式可以进一步简化为:
(2)实例方法引用
如果某个Lambda表达式里只是调用了一个实例方法,且前后参数形式一致,就可以使用实例方法引用:对象名::实例方法名
例如还是上面的例子:
我们创建一个类,在该类中创建一个实例方法来封装上述方法体中的内容:
此时Lambda可以改写为:
此时在Lambda表达式中就是只调用一个实例方法,且前后参数的形式一致,因此Lambda表达式可以进一步简化为:
(3)特定类型方法引用
如果某个Lambda表达式里只是调用一个实例方法,并且前面参数列表中的第一个参数是作为方法的主调,后面的所有参数都是作为该实例方法的入参,此时就可以使用特定类型的方法引用:类型::方法
补充:字符串的compareToIgnoreCase方法
现在对上述匿名内部类进行简化:
此时在Lambda表达式中就是只调用一个实例方法,且前面参数列表中的第一个参数是作为方法的主调,后面的所有参数都是作为该实例方法的入参,因此Lambda表达式可以进一步简化为:
(4)构造器引用---很少见
如果某个Lambda表达式里只是在创建对象,并且前后参数情况一致,此时就可以使用构造器引用:类名::new
我们定义一个Car实体类:
接下来对上述匿名内部类进行简化:
此时该Lambda表达式只是在创建对象,并且前后参数情况一致,因此Lambda表达式可以进一步简化为:
二、常见算法、正则表达式、异常
1、常见算法
(1)冒泡排序算法
每次从数组中找出最大值放到数组的最后面。
(2)选择排序算法
每轮选择当前位置,开始找出后面的较小值与该位置交换。
上述代码可以优化,显然每次固定i都要和所有的j进行比较如果符合条件就要交换,每次交换都会影响性能。因此当固定i时,可以先从所有的j中找出一个最小的j,然后在与i比较看是否交换,这样可以保证每轮只交换一次:
package com.zxy.abstractlearn;
import java.util.Arrays;
public class ChooseSortAlgormith {
public static void main(String[] args) {
int[] arr = {5, 1, 3, 2, 4, 6, 9, 7, 10, 8};
chooseSort(arr);
}
public static void chooseSort(int[] arr){
for (int i = 0; i < arr.length - 1; i++) {
int index = i + 1;
for (int j = i + 2; j < arr.length; j++) {
if (arr[index] > arr[j]){
index = j;
}
}
if (arr[i] > arr[index]){
int temp = arr[index];
arr[index] = arr[i];
arr[i] = temp;
}
}
System.out.println(Arrays.toString(arr));
}
}
(3)折半查找算法
前提条件:数组中的数据必须是有序的。它每次排除一半的数据。
package com.zxy.abstractlearn;
public class Zhebansearch {
public static void main(String[] args) {
int[] arr = {7, 23, 79, 81, 103, 127, 131, 147};
search(arr, 8);
}
public static void search(int[] arr, int number){
int left = 0;
int right = arr.length - 1;
int mid;
while (true){
if (left <= right){
// 合法的left与right
mid = (left + right) / 2;
if (arr[mid] > number){
// right 左移
right = mid - 1;
}else if (arr[mid] < number){
// left 右移
left = mid + 1;
}else {
// 找到了
System.out.println(number + "在数组中的索引是:" + mid);
return;
}
}else {
System.out.println("数组中没有这个数~~");
return;
}
}
}
}
其实java对于常见的算法已经写好了,我们可以直接用:
2、正则表达式
(1)什么是正则表达式
正则表达式就是由一些特定的字符组成,代表的是一个规则。可以用来校验数据格式是否合法;可以在一段文本中查找满足要求的内容(爬取信息);
需求:校验qq号码是否正确,要求全部是数字,长度为6-20位之间,不能以0开头:
首先我们不使用正则表达式进行校验:
使用正则表达式:(使用String类提供的实例方法)
(2)书写规则
package com.itheima.d2_regex;
/**
* 目标:掌握正则表达式的书写规则
*/
public class RegexTest2 {
public static void main(String[] args) {
// 1、字符类(只能匹配单个字符)
System.out.println("a".matches("[abc]")); // [abc]只能匹配a、b、c
System.out.println("e".matches("[abcd]")); // false
System.out.println("d".matches("[^abc]")); // [^abc] 不能是abc
System.out.println("a".matches("[^abc]")); // false
System.out.println("b".matches("[a-zA-Z]")); // [a-zA-Z] 只能是a-z A-Z的字符
System.out.println("2".matches("[a-zA-Z]")); // false
System.out.println("k".matches("[a-z&&[^bc]]")); // : a到z,除了b和c
System.out.println("b".matches("[a-z&&[^bc]]")); // false
System.out.println("ab".matches("[a-zA-Z0-9]")); // false 注意:以上带 [内容] 的规则都只能用于匹配单个字符
// 2、预定义字符(只能匹配单个字符) . \d \D \s \S \w \W
System.out.println("徐".matches(".")); // .可以匹配任意字符
System.out.println("徐徐".matches(".")); // false
// \转义
System.out.println("\""); // "
// \n \t \s(本身就是表示一个空格)
System.out.println("3".matches("\\d")); // \d: 0-9
System.out.println("a".matches("\\d")); //false
System.out.println(" ".matches("\\s")); // \s: 代表一个空白字符
System.out.println("a".matches("\s")); // false
System.out.println("a".matches("\\S")); // \S: 代表一个非空白字符
System.out.println(" ".matches("\\S")); // false
System.out.println("a".matches("\\w")); // \w: [a-zA-Z_0-9]
System.out.println("_".matches("\\w")); // true
System.out.println("徐".matches("\\w")); // false
System.out.println("徐".matches("\\W")); // [^\w]不能是a-zA-Z_0-9
System.out.println("a".matches("\\W")); // false
System.out.println("23232".matches("\\d")); // false 注意:以上预定义字符都只能匹配单个字符。
// 3、数量词: ? * + {n} {n, } {n, m}
System.out.println("a".matches("\\w?")); // ? 代表0次或1次
System.out.println("".matches("\\w?")); // true
System.out.println("abc".matches("\\w?")); // false
System.out.println("abc12".matches("\\w*")); // * 代表0次或多次
System.out.println("".matches("\\w*")); // true
System.out.println("abc12张".matches("\\w*")); // false
System.out.println("abc12".matches("\\w+")); // + 代表1次或多次
System.out.println("".matches("\\w+")); // false
System.out.println("abc12张".matches("\\w+")); // false
System.out.println("a3c".matches("\\w{3}")); // {3} 代表要正好是n次
System.out.println("abcd".matches("\\w{3}")); // false
System.out.println("abcd".matches("\\w{3,}")); // {3,} 代表是>=3次
System.out.println("ab".matches("\\w{3,}")); // false
System.out.println("abcde徐".matches("\\w{3,}")); // false
System.out.println("abc232d".matches("\\w{3,9}")); // {3, 9} 代表是 大于等于3次,小于等于9次
// 4、其他几个常用的符号:(?i)忽略大小写 、 或:| 、 分组:()
System.out.println("abc".matches("(?i)abc")); // true
System.out.println("ABC".matches("(?i)abc")); // true
System.out.println("aBc".matches("a((?i)b)c")); // true
System.out.println("ABc".matches("a((?i)b)c")); // false
// 需求1:要求要么是3个小写字母,要么是3个数字。
System.out.println("abc".matches("[a-z]{3}|\\d{3}")); // true
System.out.println("ABC".matches("[a-z]{3}|\\d{3}")); // false
System.out.println("123".matches("[a-z]{3}|\\d{3}")); // true
System.out.println("A12".matches("[a-z]{3}|\\d{3}")); // false
// 需求2:必须是”我爱“开头,中间可以是至少一个”编程“,最后至少是1个”666“
System.out.println("我爱编程编程666666".matches("我爱(编程)+(666)+"));
System.out.println("我爱编程编程66666".matches("我爱(编程)+(666)+"));
}
}
(3)应用案例
需求:校验用户输入的电话、邮箱是否合法
接下来我们使用正则表达式进行搜索替换和内容分割,需要结合String提供的如下方法:
3、异常
(1)认识异常
java程序如果遇到异常,它首先会将该异常封装成一个异常对象,然后将异常对象抛给JVM虚拟机,JVM收到异常对象后会将程序停止,并打印异常信息。
异常的体系:
Error:代表系统级别的错误(属于严重问题),也就是说系统一旦出现问题,sun公司会把这些问题封装成Error对象给出来,说白了,Error是给sun公司自己用的,不是给我们程序员用的。
Exception:叫异常,它代表的才是我们程序可能出现的问题,所以我们通常会用Exception以及它的子类来封装程序出现的异常。
Exception又分为两类:
运行时异常:RuntimeException及其子类,表示编译阶段不会出现错误提醒,运行时出现的异常(如:数组索引越界异常)
编译时异常:编译阶段就会出现错误提醒(如:日期解析异常)
上述日期解析异常就是编译时异常,java识别到这段代码很可能会出现错误,即source必须与pattern格式一致,否则无法完成解析。java担心我们所写的source与pattern不一致,因此这里报出编译时异常提醒我们再好好检查一遍。当我们确定source与pattern一致时,此时有两种方法让程序不报错:方法一:选中所有代码Ctrl + Alt + T选中6 try/catch;方法二:Alt + 回车抛出异常;
(2)自定义异常
java无法为这个世界上全部的问题都提供异常来代表,如果企业自己的某种问题,想通过异常来表示,以便用异常来管理该问题,那就需要自己来定义异常类了。
然而,年龄非法异常在java内部并没有一个具体的异常表示类,因此需要我们自定义异常类来封装这个问题。如何自定义异常类呢?我们知道异常有两个体系:运行时异常(继承RuntimeException类)和编译时异常(直接继承Exception类),那我们自定义的异常也一定是属于这两个体系中的一个,因此自定义异常也分为两大类:
注意:自定义异常类的命名:该异常是什么问题就定义成什么名字。
A、自定义运行时异常:
message存储的就是出现异常的原因。
B、自定义编译时异常
同理在方法内部用一个对象封装这个问题并抛出该对象,但由于是编译时异常,因此new出来的对象会直接报错,需要我们利用throws将错误本身抛给上层调用者,这样就和上文的parse日期解析异常一样了。无论我们输入的是正确还是错误,都会有异常提示信息。
java识别到这段代码很可能会出现错误。这里报出编译时异常提醒我们再好好检查一遍。当我们确定没问题时,此时有两种方法进行异常处理:方法一:选中所有代码Ctrl + Alt + T选中6 try/catch;方法二:Alt + 回车抛出异常;
思考:什么时候使用运行时异常,什么时候使用编译时异常呢?一般比较严重的问题需要使用编译时异常,来强烈提醒用户。
自定义运行时异常,不管代码正确与否都不提醒;自定义编译时异常不管代码正确与否都提醒报错。此时如果遇到报错,需要对异常进行处理。
(3)异常的处理
在代码层面的异常处理有两种方式:方法一:选中所有代码Ctrl + Alt + T选中6 try/catch(Alt + 回车选第二个);方法二:Alt + 回车选第一个抛出异常;
开发中对于异常的常见处理方式:
当然,当所有异常抛给最外层调用者A后还可以抛给JVM,但是不建议这样(不符合编程规范)。
上述代码还可以简化,假设我们当前的程序可能throws上百种异常,此时就需要在最上层调用者哪里调用上百次catch捕获异常,这显然很麻烦。此时可以直接抛出Exception,捕获Exception代表抛出/捕获所有异常(推荐写法):
假设我们有以下程序,它是否有问题呢?
这段代码是有问题的,因为我们使用sc.nextDouble()来接收用户的输入,有的用户就不老实输入aaaaa字符串,显然sc.nextDouble()只能接收double类型的数据,此时程序就报错直接退出了显的特别不友好。我们的想法是当程序捕获到异常时,能够重新修复提示用户输入的价格不合适,请再次输入。
由于输入的不合法导致sc.nextDouble()出现异常,系统会自动抛出到最上层的main方法,然后由于main方法中没有对异常的处理,因此程序直接挂掉了,并输出错误信息。
三、集合
前面所学的ArrayList集合只是众多集合的一种,为了满足不同业务的需求,java还提供了很多不同特点的集合供我们去使用。Java中的集合分为两大体系:Collection(单列集合,每个元素只包含一个值)和Map(双列集合,每个元素包含两个值,键值对)
1、Collection集合
(1)Collection集合体系结构
Collection集合体系:Collection是一个泛型接口,该接口只规定了单列集合的基本特点(比如,增删改查),在Collection接口下还有很多子接口:List,Set,......,不同的子接口定义了单列集合的独有特点。在子接口下面是很多很多的实现类(具体的集合),用于不同的业务需求。
Collection集合特点(Collection集合并没有定义集合过多的特点,它只是简单定义单例集合的基本特点,Collection集合的子集合List,Set才具体规定了各自系列集合具体的特点)
List系列集合:添加的元素是有序,可重复,有索引的。(ArrayList,LinkedList都是有序,可重复,有索引的。它们的区别是底层实现不同!适合的场景不同!)
有序:添加的数据是:1,2,3;按顺序取出也是:1,2,3;这就是有序的。
可重复:可添加重复的数据:1,1,2,1;
有索引:可以通过索引获取数据:arr[5]
Set系列集合:添加的元素是无序,不重复,无索引的。
HashSet:无序,不重复,无索引的。
LinkedHashSet:有序,不重复,无索引的。
TreeSet:按照大小默认升序排序,不重复,无索引的。
验证各集合的特点: ArrayList:
HashSet:
显然HashSet是无序的,正是因为它是无序的,因此它不支持索引。
(2)Collection的常用方法
Collection是单列集合的祖宗,它规定的方法所有单列集合都会继承(可以使用)。
(3)Collection的遍历方式
Collection是单列集合的祖宗,它能够使用的遍历方式,其他所有的单列集合都可以使用。Collection的遍历方式有三种形式:迭代器、增强for、lambda表达式。
思考:为什么Collection的遍历方式不支持普通for呢?因为要想使用普通for遍历,集合必须有索引,显然并不是所有的单列集合都有索引。
A:迭代器
迭代器是用来遍历集合的专用方法(数组没有迭代器),在java中迭代器的代表是lterator。
B、增强for循环
注意:增强for循环既可以遍历集合也可以遍历数组。增强for遍历集合,本质就是迭代器遍历集合的简化写法。
C、lambda表达式
注意:forEach方法需要传入一个Consumer(它是一个接口)类对象,需要使用匿名内部类。
这里的String s就好比增强for循环中的(类型 变量名)可以循环遍历集合的每个元素(利用forEach进行集合的遍历其本质也是增强for循环)
显然println就是一个PrintStream的实例方法,System.out是对象,可以使用实例方法引用:
(4)案例-遍历集合中的自定义对象
首先创建一个Movie类:
注意:
由于String类重写了toString方法因此可以直接看到内容,Movie类没有重写,因此输出的是地址。我们可以在Movie类中重写toString方法,使其直接输出内容:
案例实现:
(5)List集合
1)特有方法
由于List集合是支持索引的,因此多了很多与索引有关的方法。
2)遍历方式
List有索引,因此它的遍历方式有:普通for循环,迭代器,增强for循环,Lambda表达式
3)ArrayList、LinkedList集合的底层原理
ArrayList集合:它是基于数组实现的。数组是有索引的,因此根据索引查询数组的内容速度很快(数组可以直接根据索引定位数据位置,查询任意数据耗时相同)。但是数组的删除效率低,因为可能需要把后面所有的数据进行前移。同时添加效率也很低,因为可能需要把后面很多数据后移,再添加元素;或者也可能需要进行数组的扩容(新创建一个扩容后的数组,将原数据的内容迁移过来,然后再进行添加操作)。
因此数组的特点是:查询快,增删慢(这同时也是ArrayList集合的特点)
ArrayList适合:根据索引查询数据,比如根据随机索引取数据,或者数据量不是很大的时候。
ArrayList不适合:数据量大的同时又要频繁的进行增删操作。
LinkedList集合:它是基于双链表实现的。数组在内存中是顺序存储的,而链表不是,它是分散存储的,数据与数据之间有指针指向下一个节点的地址。想要了解双链表就需要先知道单链表。
单链表的特点:查询慢,无论查询那个数据都要从头开始找(链表在内存中不连续存放)。单链表增删相对快,因为不涉及数据的移动,只需要修改指针即可。
删除D,只需要修改C的地址指向E,然后删除D即可。
双向链表的特点:查询慢(比单向链表好点),增删相对较快,对首尾元素进行增删改查的速度是极快的(核心特点)。(同时也是LinkedList集合的特点)
LinkedLink集合基于上述特点,新增了很多首尾操作的特有方法:
LinkedList集合的应用场景:第一可以用来设计队列(先进先出),队列会频繁操作头尾数据,在尾部添加数据,删除头部数据,且集合有序。这很符合LinkedList集合的特点。
第二可以用来设计栈(先进后出),栈同样会频繁的对栈顶进行操作。在栈顶添加元素(进栈push),在栈顶删除元素(出栈pop),且数据有序。存在对首部频繁的增删操作,符合LinkedList集合特点。
在JDK1.6以后java为LinkedList集合提供了单独的入栈出栈的api:push()pop(),它们的本质还是addFirst()和removeFirst()
(6)Set集合
Set系列集合特点:无序,不重复,无索引
HashSet:无序,不重复,无索引 (使用较多)
LinkedHashSet:有序,不重复,无索引
TreeSet:排序(默认升序),不重复,无索引
Set系类集合几乎没有额外新增一些常用方法,Collection提供的方法就是Set的方法。
1)HashSet集合的底层原理
首先需要先了解哈希值:哈希值就是一个int类型的数值,java中每个对象都有一个hash值。java中的所有对象都可以调用Obejct类提供的hashCode方法,返回该对象自己的hash值:
hash值的特点:同一个对象多次调用hashCode()方法返回的hash值是相同的。不同的对象,它们的hash值一般不相同,但也有可能相同(hash冲突/碰撞)
HashSet集合是基于哈希表实现的。哈希表是一种增删改查数据,性能都较好的数据结构。在JDK8之前哈希表 = 数组 + 链表;在JDK8之后哈希表 = 数组 + 链表 + 红黑树。
JDK8之前HashSet集合的底层原理,基于哈希表:数组 + 链表
HashSet集合每次添加元素,其位置并不是顺序一个一个添加,而是根据hash取模确定位置的,因此可能第一次添加的数据在后面,而后面添加的数据反而在前面(无序,注意这里的无序只是在最开始构建的时候表现出无序,一旦结构形成就元素的位置就固定了,即无序只有一次,并不是每次输出结果都不一样),在添加元素时会调用equals方法比较不重复才允许添加。因为无序所以不支持索引。
在查询数据时,HashSet会先计算该数据的hash取模值,根据数组直接定位该数据的局部位置,然后根据链表查找。相对来讲比数组慢一点,比链表要快,因此查询速度较好。由于查询速度较好,因此HashSet集合增删操作也比较不错(不涉及数据的移动)。
思考:如果数组快占满了,该怎么办呢?当数组快满以后再添加数据(大概率会添加到已存在的链表下),会导致数组下的链表越来越长,链表过长会导致查询性能的降低,此时需要对哈希表扩容(也就是对数组扩容)。加载因子就是用于控制数组什么时候扩容的,对数组进行扩容并不是等所有空位置都占满元素才扩,当数组中有数组长度 * 加载因子(16 * 0.75 = 12)个位置被占满后就会进行扩容。
思考:如何扩容?系统会重新创建一个是原来数组长度2倍的新数组,然后将原数组中的所有元素重新进行hash取模,放到新数组中,这样可以使数据分散开来减小链表的长度。
思考:如果出现下面情况怎么办呢?
JDK8之后HashSet集合的底层原理,基于哈希表:数组 + 链表 + 红黑树
JDK8开始,当链表长度>8,且数组长度>=64时,自动将链表长度>8的链表转成红黑树,这样可以进一步提高操作数据的性能。
红黑树是一种自平衡二叉树,它是一种增删改查数据性能相对都较好的结构:
深入理解HashSet集合去重复的机制:HashSet集合默认不能对内容一样的两个不同对象去重复!显然内容一样的不同对象hash值一定不同,不能去重。
如何让HashSet集合能够实现对内容一样的两个不同对象也能去重复呢?需要保证两个内容一样的不同对象经过hash计算,结果相同。然后使用equals方法进行内容比较。此时我们需要重写hashCode()和equals()方法。
此时再次执行main函数,就会只有三个Studnet对象。
2)LinkedHashSet集合的底层原理
LinkedHashSet集合依然是基于哈希表(数组,链表,红黑树)实现的。但是它的每个元素都额外的多了一个双链表的机制记录它前后元素的位置(核心机制)。
红色指针是头节点(指向第一个插入元素对应的数组),橙色是尾节点(指向最后一个元素指向的数组),每次添加新元素,都会先根据尾节点寻找当前最后一个元素,然后将新元素与尾节点指向的元素建立双链表,并修改尾节点指向。
当我们输出LinkedHashSet集合中的元素时,会先找到头节点,按照链条依次输出,显然这也是我们添加元素的顺序,因此LinkedHashSet是有序的。它的基本结构还是遵循HashSet的规则,会有equals过程,因此是不重复的。而且每个元素的位置在哪里还是基于hash取模,因此不支持索引。LinkedHashSet集合依然是基于哈希表,因此LinkedHashSet集合增删改查性能都比较好。
LinkedHashSet集合也是有缺点的:占内存,因为每个元素都额外记录它前后元素的位置。
3)TreeSet集合的底层原理
特点:可排序(默认升序排序),不重复,无索引。TreeSet集合是基于红黑树实现排序的,因此TreeSet集合的增删改查都比较好。由于红黑树本身已经排好序了:左子树<根<右子树。因此在输出元素时会默认升序排序。例如插入元素6,5,7。输出5,6,7。
对于数值类型:Integer,Double,默认按照数值本身的大小进行升序排序;对于字符串类型:默认按照首字符的编号升序排序。对于自定义类型如Student对象,TreeSet集合默认是无法直接排序的(它不知道按照什么规则进行排序,是按照name,age,还是height?)。
此时需要自定义排序规则,有两种方式自定义规则:
方式一:
再次执行main函数:(牛魔王的年龄重复TreeSet不存储)
方式二:
上述匿名内部类可以简化。
(7)集合的并发修改异常问题
使用迭代器遍历集合时,又同时在删除集合中的数据,此时程序就会出现并发修改异常的错误。
利用迭代器读取数据的同时,删除list中的元素,但是迭代器的指针一直在往后走,这会导致有部分元素没有经过判断,比如李爱花、李玉刚。
解决方法:
注意:如果使用增强for循环和Lambda表达式来遍历集合并删除数据,也会出现并发修改异常的问题,且异常无法解决。
(8)Collection集合总结:
2、Collections工具类
(1)可变参数
本质上就是一种特殊的形参,定义在方法、构造器的形参列表中,格式是:数据类型...参数名称;
可变参数特点:可以不传数据给它;可以传入一个或者同时传入多个数据给它;也可以传入一个数组给它。可以灵活的接收数据。
可变参数对外可以灵活的接收参数,对内就是一个数组。
注意事项:A、一个形参列表中,只能有一个可变参数
B、可变参数必须放在形参列表的最后面
(2)Collections类
是一个用来操作集合的工具类。
针对自定义类型的对象sort方法无法直接进行排序。
解决方法还是那两个。一是在Student类中定义比较规则。二是使用sort()的重载方法自定义比较规则。
(3)综合案例
打牌我们不实现,因为需要涉及一些复杂算法,以及信息通信。
每个牌就是一个对象,我们需要先创建一个牌类对象Card,存储一张牌的信息。
此外需要创建一个房间对象Room:
创建一个游戏测试类:GameDemo
54张牌已经生成了,且大小都没问题,此时就可以修改Card类中的toString方法,让其不显示size大小了,我们心里知道即可。
房间构建出来了,并且54张牌也构建出来了,此时就需要启动游戏了,因此在Room类中定义一个start方法,进行游戏启动:
此时51张牌全发完了,按理说应该进行抢地主了,然后谁抢到地主就把剩余3张牌发给他。但是这里我们就不实现了,因为也涉及复杂算法,以及信息通信(我们假设某个人是地主)。接下来对3个人的牌进行排序,并看牌。先实现看牌:
设计排序,排序是一个功能,我们使用一个方法处理:
此时注意到,抢完地主以后还需要为地主重新排序:
补充:
(1)UUID.randomUUID()
UUID类下有一个randomUUID()静态方法,可以随机生成一个长度为36位的由数字,小写字母,-(4位)组成的字符。一般用于生成主键(用户编号,员工编号),因为可以保证每次生成的字符都不同。
(2)Scanner中next()与nextLine()的区别
nextLine()方法接收一整行的全部数据。而next()在遇到一些特殊符号时会中断: