目录
Java集合(二)
Collections集合工具类
泛型
泛型介绍
定义泛型
类型限定符
泛型通配符
泛型通配符与类型限定符结合
斗地主案例
Set接口
Set接口介绍
HashSet类
LinkedHashSet类
哈希值介绍
Java中计算哈希值的方法
HashSet去重的方式
Java集合(二)
Collections
集合工具类
不同于Collection
,Collections
是一个工具类,所以其构造方法为私有方法,成员方法为静态
常用方法:
static <T> boolean addAll(Collection<? super T> c, T... elements)
:批量向指定集合添加数据,第二个参数为一个可变参数列表,表示可以一次性添加多个元素static void shuffle(List<?> list)
:随机打乱单列集合中的元素(每一次运行结果都不一样)static <T> void sort(List<T> list)
:使用单列集合中的泛型实现的Comparable
接口中的方法对集合中的数据进行排序。如果泛型对应的类没有实现Comparable
接口,则编译报错static <T> void sort(List<T> list, Comparator<? super T> c)
:使用自定义实现Comparator
接口中的方法对指定集合中的数据进行排序
基本使用如下:
java">public class Test {public static void main(String[] args) {ArrayList<String> strings = new ArrayList<>();// 1. static <T> boolean addAll(Collection<? super T> c, T... elements)Collections.addAll(strings,"abc","sac","bsd");for (String string : strings) {System.out.println(string);}System.out.println();// 2. static void shuffle(List<?> list)Collections.shuffle(strings);for (String string : strings) {System.out.println(string);}System.out.println();// 3. static <T> void sort(List<T> list)Collections.sort(strings); // String 实现了 Comparable接口for (String string : strings) {System.out.println(string);}System.out.println();// 4. static <T> void sort(List<T> list, Comparator<? super T> c)ArrayList<Person> people = new ArrayList<>();people.add(new Person(23,"张三"));people.add(new Person(12,"李四"));people.add(new Person(45,"王五"));Collections.sort(people, new Comparator<Person>() {@Overridepublic int compare(Person o1, Person o2) {return o1.getAge() - o2.getAge();}});for (Person person : people) {System.out.println(person);}}
}
泛型
泛型介绍
泛型:不明确具体类型,直到接收到具体类型再进行推导
使用泛型有两个原因:
- 统一数据类型,防止使用时出现的数据类型转换异常
- 定义带泛型的类,方法等,使用的时候给泛型确定什么类型,泛型就会自动推导为对应类型,使代码更灵活
定义泛型
定义泛型一共有三个位置:
- 定义泛型的类:在类名后面添加
<T>
,其中<>
表示泛型,T
为泛型名,类似于变量名,对于类泛型来说,当该类实例化出对象时,泛型就会被替代为指定类型,格式如下:
java">public class 类名 <T> {// 成员
}
基本使用如下:
java">public class MyArrayList <E> {//定义一个数组,充当ArrayList底层的数组,长度直接规定为10Object[] obj = new Object[10];//定义size,代表集合元素个数int size;/*** 定义一个add方法,参数类型需要和泛型类型保持一致** 数据类型为E 变量名随便取*/public boolean add(E e) {obj[size] = e;size++;return true;}/*** 定义一个get方法,根据索引获取元素*/public E get(int index) {return (E) obj[index];}@Overridepublic String toString() {return Arrays.toString(obj);}
}// 测试
public class Test {public static void main(String[] args) {MyArrayList<String> list1 = new MyArrayList<>();list1.add("aaa");list1.add("bbb");System.out.println(list1); //直接输出对象名,默认调用toStringSystem.out.println("===========");MyArrayList<Integer> list2 = new MyArrayList<>();list2.add(1);list2.add(2);Integer element = list2.get(0);System.out.println(element);System.out.println(list2);}
}
需要注意,之所以定义元素Object
类型的数组是为了便于做强制类型转换,因为泛型不是具体类型,对于get
方法来说,当泛型作为一个方法的返回值时,返回值需要进行强制转换,此时因为Object
是所有类的父类,所以此时可以强制转换,例如源码迭代器中的next
方法返回值
java">public E next() {// ...return (E) elementData[lastRet = i];
}
- 定义泛型方法:泛型方法在方法被调用时泛型被推导为具体类型。基本格式如下:
java">权限修饰符 其他修饰符 <T> 方法返回值 方法名(泛型形参列表) {// 方法体
}
基本使用如下:
java">public class ListUtils {//定义一个静态方法addAll,添加多个集合的元素public static <E> void addAll(ArrayList<E> list,E...e){for (E element : e) {list.add(element);}}}// 测试
public class Test01 {public static void main(String[] args) {ArrayList<String> list1 = new ArrayList<>();ListUtils.addAll(list1,"a","b","c");// 也可以写成如下形式// ListUtils.<String>addAll(list1,"a","b","c");System.out.println(list1);System.out.println("================");ArrayList<Integer> list2 = new ArrayList<>();ListUtils.addAll(list2,1,2,3,4,5);System.out.println(list2);}
}
- 定义泛型接口:泛型接口在实现类实例化对象时或者实现类本身指定了具体类型,泛型才会被推导为具体类型。基本格式如下,与定义泛型类基本一致:
java">public interface 接口名 <T> {// 成员
}
基本使用如下:
- 实现类实例化对象时确定类型
java">// 泛型接口
public interface MyList <E> {public boolean add(E e);
}// 接口实现类
public class MyArrayList1<E> implements MyList<E> {//定义一个数组,充当ArrayList底层的数组,长度直接规定为10Object[] obj = new Object[10];//定义size,代表集合元素个数int size;/*** 定义一个add方法,参数类型需要和泛型类型保持一致** 数据类型为E 变量名随便取*/public boolean add(E e) {obj[size] = e;size++;return true;}/*** 定义一个get方法,根据索引获取元素*/public E get(int index) {return (E) obj[index];}@Overridepublic String toString() {return Arrays.toString(obj);}
}// 测试
public class Test02 {public static void main(String[] args) {MyArrayList1<String> list1 = new MyArrayList1<>();list1.add("张三");list1.add("李四");System.out.println(list1.get(0));}
}
- 实现类已经指定了具体的类型
java">// 接口
public interface MyIterator <E> {E next();
}// 实现类指定了具体类型为String
public class MyScanner implements MyIterator<String> {@Overridepublic String next() {return "实现类指定具体类型时推导泛型";}
}// 测试
public class Test03 {public static void main(String[] args) {MyScanner myScanner = new MyScanner();String result = myScanner.next();System.out.println("result = " + result);}
}
类型限定符
在Java中,可以通过两个关键字限制方法或类时传递给泛型的类型
extends
:限制传递给模版的具体类型为extends
后的本类或者子类类型,基本语法如下:
java">模版参数类型 extends 具体类型// 例如
// 类
public class Test04 <T extends String>{
}// 方法
public <T extends String> void test(T t) {System.out.println(t);
}
super
:限制传递给模版的具体类型为super
后的本类或者父类类型
java">模版参数类型 super 具体类型// 使用方式同extends
泛型通配符
如果需要确保传递给泛型的具体类型为任意引用数据类型,可以在<>
中使用?
占位,表示泛型通配符,例如:
java">public class Test05 {public static void main(String[] args) {ArrayList<String> list1 = new ArrayList<>();list1.add("张三");list1.add("李四");ArrayList<Integer> list2 = new ArrayList<>();list2.add(1);list2.add(2);method(list1);method(list2);}public static void method(ArrayList<?> list){for (Object o : list) {System.out.println(o);}}}
泛型通配符与类型限定符结合
例如下面的代码:
java">public class Test06 {public static void main(String[] args) {ArrayList<Integer> list1 = new ArrayList<>();ArrayList<String> list2 = new ArrayList<>();ArrayList<Number> list3 = new ArrayList<>();ArrayList<Object> list4 = new ArrayList<>();get1(list1);//get1(list2);错误get1(list3);//get1(list4);错误System.out.println("=================");//get2(list1);错误//get2(list2);错误get2(list3);get2(list4);}//上限 ?只能接收extends后面的本类类型以及子类类型public static void get1(Collection<? extends Number> collection){}//下限 ?只能接收super后面的本类类型以及父类类型public static void get2(Collection<? super Number> collection){}
}
斗地主案例
案例介绍:
按照斗地主的规则,完成洗牌发牌的动作。 具体规则:
使用54张牌打乱顺序,三个玩家参与游戏,三人交替摸牌,每人17张牌,最后三张留作底牌
案例分析:
- 准备牌:
牌可以设计为一个ArrayList<String>
,每个字符串为一张牌。 每张牌由花色数字两部分组成,我们可以使用花色集合与数字集合嵌套迭代完成每张牌的组装。 牌由Collections
类的shuffle
方法进行随机排序。
- 发牌
将每个人以及底牌设计为ArrayList<String>
,将最后3张牌直接存放于底牌,剩余牌通过对3取模依次发牌。
- 看牌
直接打印每个集合
java">public class Test_Poker {public static void main(String[] args) {// 创建花色ArrayList<String> color = new ArrayList<>();Collections.addAll(color, "黑桃", "红心", "梅花", "方块");// 创建号牌ArrayList<String> number = new ArrayList<>();Collections.addAll(number, "2", "A", "K", "Q", "J", "10", "9", "8", "7", "6", "5", "4", "3");// 创建牌盒ArrayList<String> pokerBox = new ArrayList<>();// 大小王pokerBox.add("大王");pokerBox.add("小王");// 发牌for (String s : color) {for (String string : number) {pokerBox.add(s + string);}}// 打乱牌盒Collections.shuffle(pokerBox);// 创建玩家ArrayList<String> player1 = new ArrayList<>();ArrayList<String> player2 = new ArrayList<>();ArrayList<String> player3 = new ArrayList<>();ArrayList<String> last = new ArrayList<>();System.out.println(pokerBox.size());// 发牌,留三张底牌for (int i = 0; i < pokerBox.size(); i++) {if (i >= 51) {last.add(pokerBox.get(i));} else if (i % 3 == 0) {player1.add(pokerBox.get(i));} else if (i % 3 == 1) {player2.add(pokerBox.get(i));} else {player3.add(pokerBox.get(i));}}// 查看System.out.println("player1: " + player1);System.out.println("player2: " + player2);System.out.println("player3: " + player3);System.out.println("last: " + last);}
}
也可以使用字符分割方法对创建花色和创建号牌进行优化
java">public class Test_Poker01 {public static void main(String[] args) {// 创建花色String[] color = "黑桃-红心-梅花-方块".split("-");// 创建号牌String[] number = "2-3-4-5-6-7-8-9-10-J-Q-K-A".split("-");// 创建牌盒ArrayList<String> pokerBox = new ArrayList<>();// 添加大小王pokerBox.add("大王");pokerBox.add("小王");// 组合其他牌for (String s : color) {for (String n : number) {pokerBox.add(s+n);}}// 创建玩家并发牌ArrayList<String> player1 = new ArrayList<>();ArrayList<String> player2 = new ArrayList<>();ArrayList<String> player3 = new ArrayList<>();// 底牌ArrayList<String> last = new ArrayList<>();for (int i = 0; i < pokerBox.size(); i++) {if(i >= 51) {last.add(pokerBox.get(i));} else if(i % 3 == 0) {player1.add(pokerBox.get(i));} else if(i % 3 == 1) {player2.add(pokerBox.get(i));} else {player3.add(pokerBox.get(i));}}// 查看System.out.println("player1: " + player1);System.out.println("player2: " + player2);System.out.println("player3: " + player3);System.out.println("last: " + last);}
}
Set
接口
Set
接口介绍
Set
接口实际上并没有对Collection
接口进行功能上的扩充,而且所有的Set
集合底层都是依靠Map
实现,部分内容将在Map
中具体介绍
Set
和Map
密切相关的
Map
的遍历需要先变成单列集合,只能变成set
集合
HashSet
类
HashSet
是Set
接口的实现类,下面是HashSet
的特点:
- 元素不允许重复
- 元素插入顺序与存储顺序不一定相同
- 没有索引的方式操作元素
- 线程不安全
- 底层数据结构:
- JDK8之前:哈希表(数组+链表实现)
- JDK8之后:哈希表(数组+链表+红黑树实现)
- 方法:与
Collection
接口中的方法基本一致,但是因为HashSet
是实现类,所以HashSet
存在具体的方法体 - 遍历方式:
- 增强
for
- 迭代器
- 增强
基本使用实例:
java">public class Test {public static void main(String[] args) {HashSet<String> set = new HashSet<>();set.add("张三");set.add("李四");set.add("王五");set.add("赵六");set.add("田七");set.add("张三");System.out.println(set);//迭代器Iterator<String> iterator = set.iterator();while(iterator.hasNext()){System.out.println(iterator.next());}System.out.println();//增强forfor (String s : set) {System.out.println(s);}}
}
LinkedHashSet
类
LinkedHashSet
是Set
接口的实现类,下面是LinkedHashSet
的特点:
- 元素唯一
- 元素插入顺序与存储顺序相同
- 没有索引的方式操作元素
- 线程不安全
- 底层数据结构:哈希表+双向链表
- 方法:与
HashSet
基本一致
基本使用如下:
java">public class Test01 {public static void main(String[] args) {LinkedHashSet<String> set = new LinkedHashSet<>();set.add("张三");set.add("李四");set.add("王五");set.add("赵六");set.add("田七");set.add("张三");System.out.println(set);//迭代器Iterator<String> iterator = set.iterator();while(iterator.hasNext()){System.out.println(iterator.next());}System.out.println();//增强forfor (String s : set) {System.out.println(s);}}
}
哈希值介绍
在Java中,哈希值是由计算机算出来的一个十进制数,可以看做是对象的地址值
需要获取对象的哈希值可以通过Object
类中的本地方法:public native int hashCode()
例如查看Person
类的两个对象的hashCode
java">public class Test {public static void main(String[] args) {Person person1 = new Person();Person person2 = new Person();System.out.println(person1.hashCode());System.out.println(person2.hashCode());}
}输出结果:
1163157884
1956725890
实际上,在Person
类中没有重写toString
方法时打印的数据后面数字对应的就是对象的hashCode
,只是显示的是对应的16进制,例如下面的代码
java">public class Test {public static void main(String[] args) {Person person1 = new Person();Person person2 = new Person();System.out.println(person1);System.out.println(person2);}
}输出结果:
com.epsda.advanced.test_HashCode.Person@4554617c
com.epsda.advanced.test_HashCode.Person@74a14482
如果将前面直接打印的hashCode
值以16进制形式打印,则此时会发现对应的数值与默认打印对象名的数据的hashCode
一致,例如下面的代码:
java">public class Test {public static void main(String[] args) {Person person1 = new Person();Person person2 = new Person();System.out.println(Integer.toHexString(person1.hashCode()));System.out.println(Integer.toHexString(person2.hashCode()));System.out.println(person1);System.out.println(person2);}
}输出结果:
4554617c
74a14482
com.epsda.advanced.test_HashCode.Person@4554617c
com.epsda.advanced.test_HashCode.Person@74a14482
所以,当对象重写了toString
方法,此时就不会打印对象的地址,实际上就是不打印对应的hashCode
如果在类中重写hashcode
,此时打印对象的hashCode
值就会根据内容进行计算,例如下面的代码:
java">// Person类
public class Person {// ...// 重写hashCode@Overridepublic int hashCode() {return Objects.hash(name, age);}
}public class Test {public static void main(String[] args) {Person person1 = new Person("张三", 23);Person person2 = new Person("张三", 23);System.out.println(person1.hashCode());System.out.println(person2.hashCode());}
}输出结果:
24022543
24022543
在上面的代码中,因为Person
类的两个对象内容一致,并且因为Person
类重写了hashCode
方法,方法中是根据成员内容进行hashCode
计算的,所以打印的hashcode
是相同的
但是,有些特殊情况,内容不同时,hashCode
可能相同,这个现象被称为哈希冲突或哈希碰撞,例如下面的代码:
java">public class Test {public static void main(String[] args) {String s1 = "通话";String s2 = "重地";System.out.println(s1.hashCode());System.out.println(s2.hashCode());}
}输出结果:
1179395
1179395
总结:
当内容相同时,hashCode
一定相同;但是内容不同时,hashCode
不一定相同
Java中计算哈希值的方法
以下面的代码为例:
java">public class Test01 {public static void main(String[] args) {String s = "abc";System.out.println(s.hashCode());}
}
对应的hashCode
源码如下:
java">public int hashCode() {int h = hash;if (h == 0 && value.length > 0) {char val[] = value;for (int i = 0; i < value.length; i++) {h = 31 * h + val[i];}hash = h;}return h;
}
上面的代码中,value
是String
底层的byte
类型数组,使用char
类型的val
数组接受,根据val
数组中的字符依次进行h = h * 31 + val[i]
进行计算直到循环结束,此时h
即为对应的hash
值
计算 hashCode
时,之所以使用31可以简单理解为31是一个质数,31这个数通过大量的计算和统计,认为用31,可以尽量降低内容不一样但是哈希值一样的情况
HashSet
去重的方式
本部分简单介绍,在 Map
部分会进行详细介绍
- 先计算元素的哈希值(重写
hashCode
方法),再比较内容(重写equals
方法) - 先比较哈希值,如果哈希值不一样,存入集合中
- 如果哈希值一样,再比较内容
- 如果哈希值一样,内容不一样,直接存入集合
- 如果哈希值一样,内容也一样,去重复内容,留一个存入集合
所以前面之所以重写了equals
方法的同时还需要重写hashCode
就是为了尽可能保证内容比较和去重的可靠性
总结:
对于自定义类型来说,如果不需要打印对象的地址而是打印对象的内容就重写toString
方法,而需要比较对象是否相同除了内容比较还需要进行hashCode
比较,所以需要重写equals
和hashCode
方法