JavaSE基础day15 泛型、双向集合、无序集合、通配符
一.泛型
(一)泛型的概述和使用
1、泛型:广泛的类型,在定义一个类的时候,类型中有些方法参数、返回值类型不确定,可以使用一个符号,来表示那些尚未确定的类型,这个符号,就称为泛型。泛型是java5中引入的特性,它提供了编译时类型安全检测机制
2、使用:在集合当中,我们去创建集合对象的时候,后面可以添加一个<引用数据类型> ,表示泛型,表示的是该集合当中,存储元素的数据类型。在集合的创建中添加了泛型之后,该集合只能存储该类型的数据,或者是该类型子类的数据,不能存储不是该类型,并且不能用该类型接受的数据。
例如:ArrayList<Integer> al = new ArrayList<Integer>();
3、泛型的好处:
(1) 提高了数据的安全性,将运行时的问题,提前暴露在编译时期
(2) 避免了强转的麻烦
4、注意事项:
(1) 前后一致:在创建对象时,赋值符号前面和后面的类型的泛型,必须一致
(2) 泛型推断:如果前面的引用所属的类型已经写好了泛型,后面创建对象的类型就可以只写一个尖括号,尖括号中可以不写任何内容。<>特别像菱形,称为“菱形泛型”,jdk1.7特性
import java.util.ArrayList; public class FanXing使用 { public static void main(String[] args) { // 1. 没有泛型时集合的使用: 缺点 1) 存储在同一个容器中的数据类型可以不一致,导致数据获取发生类型转换错误 // 2) 当需要获取集合元素时,做多态向下转型,麻烦 ArrayList list = new ArrayList(); list.add("hello"); list.add(15); list.add(3.14); /* for(Object obj : list){ String s = (String)obj; System.out.println(s); }*/ // 2. 有泛型集合使用: 当创建一个集合对象时, 可以设定出集合中具体的泛型类型 // list1集合中只能存储Integer类型和其子类类型 ArrayList<Integer> list1 = new ArrayList<>(); list1.add(15); // 泛型好处: 1) 将没有泛型时,运行时期的异常前置, 在编译时期不允许其他无关类型数据存储在集合中 // 2) 不需要再进行多态的向下转型了 list1.add(-18); for(Integer i : list1){ System.out.println(i); } ArrayList<String> list2 = new ArrayList<>(); list2.add("hi"); // 3. 泛型写作注意事项 // 1. 前后一致 ArrayList<Double> list3 = new ArrayList<Double>(); // ArrayList<Object> list4 = new ArrayList<Double>(); // 2. 到JDK1.7版本, 简化了后面泛型写作, 后面泛型可以不写, 默认与前面泛型保持一致 ArrayList<String> list5 = new ArrayList<>(); list5.add("hi"); String str = list5.get(0); System.out.println(str);// hi ArrayList<String> list6 = new ArrayList(); } }
迭代器中的泛型
import java.util.ArrayList; import java.util.Iterator; import java.util.ListIterator; public class FanXing在迭代器中的使用 { public static void main(String[] args) { ArrayList<Integer> list1 = new ArrayList<>(); list1.add(15); list1.add(18); list1.add(-9); Iterator<Integer> it = list1.iterator(); while(it.hasNext()){ int i = it.next(); System.out.println(i); } ListIterator<Integer> it1 = list1.listIterator(); while(it1.hasNext()){ int i = it1.next(); System.out.println(i); } } }
(二)泛型类的定义
1、泛型类:带着泛型定义的类
2、格式:
class 类名<泛型类型1, 泛型类型2, .....> {
}
3、说明:
(1) 类名后面跟着的泛型类型,是泛型的声明,一旦泛型声明出来,就相当于这个类型成为了已知类型,这个类型就可以在整个类中使用
(2) 泛型的声明名称,只需要是一个合法的标识符即可,但是通常我们使用单个大写字母来表示,常用字母:T、W、Q、K、V、E
(3) 泛型确定的时机:将来在使用和这个类,创建对象的时候
import java.util.ArrayList; // 1. 类上的泛型在类型中当做一个已知的数据类型进行使用(泛型未来可以代表一种引用数据类型) // 菱形尖括号中的E, 就表示泛型类型 // 泛型使用一个大写英文字母进行表示: 英文字母随意; 通常使用一些常见英文单词的首字母 K(key) V(value) E(element) T(type), W, Q, A... public class FanXingClass<E> { // 2. 类上的泛型E在类型中可以进行使用 ArrayList<E> list = new ArrayList<>(); public void add(E e){ list.add(e); System.out.println(list); } public E get(int index){ return list.get(index); } }
public class TestFanXing { public static void main(String[] args) { // 2. 类上的泛型具体类型确定时机: 在创建这个类对象同时(实例化对象同时) FanXingClass<String> fxc = new FanXingClass(); fxc.add("857"); fxc.add("你好"); String first = fxc.get(0); System.out.println(first);// 857 } }
(三)泛型方法的定义
1、在方法声明中,带着泛型声明的方法,就是泛型方法
2、格式:
修饰符 <泛型声明1, 泛型声明2,.....> 返回值类型 方法名称(参数列表) {
}
3、说明:
(1) 在方法上声明的泛型,可以在整个方法中,当做已知类型来使用
(2) 如果【非静态】方法上没有任何泛型的声明,那么可以使用类中定义的泛型
(3) 如果【静态】方法上没有任何的泛型声明,那么就不能使用泛型,连类中定义的泛型,也不能使用,因为类中的泛型需要在创建对象的时候才能确定。所以【静态】方法想使用泛型,就必须在自己的方法上单独声明。
import java.util.ArrayList; // 1. 类上的泛型在类型中当做一个已知的数据类型进行使用(泛型未来可以代表一种引用数据类型) // 菱形尖括号中的E, 就表示泛型类型 // 泛型使用一个大写英文字母进行表示: 英文字母随意; 通常使用一些常见英文单词的首字母 K(key) V(value) E(element) // T(type), W, Q, A... public class FanXingClass<E> { // 2. 类上的泛型E在类型中可以进行使用 ArrayList<E> list = new ArrayList<>(); public void add(E e){ list.add(e); System.out.println(list); } public E get(int index){ return list.get(index); } // 静态方法不能使用类上泛型: 如果想使用泛型只能在静态方法上自己定义 // 类上泛型具体数据类型确定时机是创建对象的同时; 而静态优先于对象存在, 可以使用静态方法的时候,E类型还没确定 // 因此不能用 // 功能: 将任意类型数组中的指定两个索引位置元素进行交换 // 方法上定义的泛型就只能在当前方法中使用 public static<W> void changeArray(W[] arr, int index, int index1){ // 记录下index索引位置数据的原值 W temp = arr[index]; arr[index] = arr[index1]; arr[index1] = temp; for(W ele : arr){ System.out.print(ele + " "); } } }
public class TestFanXing { public static void main(String[] args) { // 2. 类上的泛型具体类型确定时机: 在创建这个类对象同时(实例化对象同时) FanXingClass<String> fxc = new FanXingClass(); fxc.add("857"); fxc.add("你好"); String first = fxc.get(0); System.out.println(first);// 857 // 3. 测试静态方法上的泛型 Integer[] arr = {12,13}; String[] arr1 = {"122","133"}; // changeArray(W[] arr, int index, int index1); FanXingClass.changeArray(arr,0,1); } }
(四)泛型通配符
1、使用泛型的时候,没有使用具体的泛型声明T,而是使用了和声明过的某个泛型T有关的一类类型,就称为泛型的通配符。三种形式:
2、第一种形式,使用?来表示可以是任意类型,例如:
Collection<E>接口中的removeAll(Collection<?> c),表示可以接收任意泛型类型的集合,作为该方法的实际参数,参数集合的泛型,可以是与E没有任何关系
3、第二种形式,使用? extends E来表示必须是某个泛型类型或是该泛型类型的子类,例如:
Collection<E>接口中的addAll(Collection<? extends E> c),表示可以接收泛型类型是调用者泛型类型或者其子类的集合,作为该方法的实际参数。参数的泛型和调用者的泛型,必须有关(相同或者是子父类)。确定了泛型的上边界。
4、第三种形式,使用? super E来表示必须是某个泛型类型或者是该泛型类型的父类,例如:
Arrays工具类中,排序方法static <T> void sort(T[] a, Comparator<? super T> c),T是该方法的泛型,T表示的是数组中元素的类型,<? super T>表示可以接收泛型类型是数组元素类型T或者是元素类型T的父类的比较器,作为sort方法的参数。参数的泛型和方法的泛型,必须有关(相同或者是子父类)。确定了泛型的下边界。
import java.util.ArrayList; public class 泛型通配符 { public static void main(String[] args) { ArrayList<String> list = new ArrayList<>(); list.add("hi"); ArrayList<Integer> list1 = new ArrayList<>(); list1.add(15); ArrayList<String> list2 = new ArrayList<>(); list.add("hello"); // removeAll(Collection<?> c): 泛型?表示任意数据类型均可,从list集合中将list1集合中的每一个元素都删除 // 包含就删除,不包含不删除 list.removeAll(list1); // addAll(Collection<? extends E> c) : 参数Collection集合提供的泛型类型?表示, 必须是E类型本身 // 或者是E类型的子类类型 //list.addAll(list1); list.addAll(list2); // ? super T : 提供的泛型类型?, 必须是T类型本身或者是T类型的父类类型 } }
二. 无序单列集合Set
(一) 概述
1. java.util.Set接口和java.util.List接口一样,同样继承自Collection接口,它与Collection接口中的方法基本一致,并没有对Collection接口进行功能上的扩充,只是比Collection接口更加严格了。与List接口不同的是,Set接口中元素无序,并且都会以某种规则保证存入
的元素不出现重复。
2. 特点:
(1) 无序:没有任何前后的分别,存入的顺序和取出的顺序不一定一致
(2) 没有索引:集合中没有任何位置,元素也就没有位置的属性
(3) 不重复:没有位置的区分,相同值的元素没有任何分别,所以不能重复
3. Set的常用实现类:HashSet
4. Set集合的遍历方式: toArray(), 迭代器遍历, 增强for遍历
import java.util.HashSet; public class SetMethod { public static void main(String[] args) { HashSet<String> set = new HashSet<>(); // 1. set集合中不存储重复元素 set.add("hi"); set.add("hello"); set.add("19"); set.add("19"); set.add("66"); // 2. Set无序集合: 存入数据的顺序和取出数据的顺序不保证一致 System.out.println(set);// [66, hi, 19, hello] // 3. addAll(Collection c): 将参数c集合中的每一个元素都添加到方法调用的set集合中 HashSet<String> set1 = new HashSet<>(); set1.add("66"); set1.add("world"); set1.add("你好"); /* set.addAll(set1); System.out.println(set);// [66, hi, world, 你好, 19, hello]*/ // 4. containsAll(Collection<?> c) : 验证方法调用集合中是否完全包含参数集合中c中的每一个元素 // System.out.println(set.containsAll(set1));// false // 5. removeAll(Collection<?> c) : 将参数c集合中的每一个元素都从方法调用集合中删除 // 换言之: 将两个集合的交集从方法调用集合中删除 /*set.removeAll(set1); System.out.println(set);// [hi, 19, hello]*/ // 6. retainAll(Collection<?> c) : 将两个集合的交集保留在方法调用集合中 set.retainAll(set1); System.out.println(set);// [66] } }
练习1
使用合适的容器存储5个不重复的1-99之间的任意随机整数
分析 :
容器: 数组, List(有序,有索引,可重复), Set(无序,无索引,不重复)
import java.util.HashSet; import java.util.Random; public class Set案例 { public static void main(String[] args) { // 使用合适的容器存储5个不重复的1-99之间的任意随机整数 HashSet<Integer> set = new HashSet<>(); Random ran = new Random(); while(set.size() < 5){ int number = ran.nextInt(99) + 1; set.add(number); } System.out.println(set); } }
(二) 数据结构之哈希表
1. 哈希值
(1) 是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值
(2) 如何获取哈希值: Object类中的public int hashCode()返回对象的哈希码值
(3) 哈希值的特点
同一个对象多次调用hashCode()方法返回的哈希值是相同的
默认情况下,不同对象的哈希值是不同的。而重写hashCode()方法,可以实现让不同对象的哈希值相同
2. 哈希表结构
(1) JDK1.8以前: 数组 + 链表
(2) JDK1.8以后:
a. 节点个数少于等于8个: 数组 + 链表
b. 节点个数多于8个: 数组 + 红黑树
(二) HashSet保证元素唯一源码分析
1、某个对象obj,在即将要存储到HashSet集合的时候,首先计算obj的hashCode值
2、在集合中的所有元素的哈希值,都和obj的哈希值不同,说明在集合中不存在obj,可以直接将obj存储到HashSet中
3、在集合中有若干元素的哈希值,和obj的哈希值相同,并不能说明obj已经存在于集合中,需要使用equals判断obj是否和那些与自己哈希值相同的元素是否相等
4、如果这些元素所有的和obj比较equals之后,都不相等,那么就说明obj不存在于集合中,可以将obj存储到HashSet中
5、如果这些元素有任意一个和obj比较equals之后,发现相等,那么就说明obj已经存在于集合中,所以obj就不能存储,存储失败
结论: 自定义引用数据类型想通过成员变量区分是否是重复对象, 需要使用alt+insert快捷键重写hashCode和equals方法
import java.util.Objects; public class Person { private String name; private int 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); } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } public Person() { } public Person(String name, int age) { this.name = name; this.age = 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; } }
import java.util.HashSet; public class HashSet保证元素唯一 { public static void main(String[] args) { HashSet<Integer> set = new HashSet<>(); set.add(15); set.add(18); set.add(15); // 1. HashSet存储JDK定义类型,直接保证元素唯一性: JDK的类,都已经重写过了HashCode和equals方法 System.out.println(set);// [18,15] // 2. HashSet存储自定义的引用数据类型: 想通过成员变量保证对象唯一性需重写hashCode和equals方法 HashSet<Person> set1 = new HashSet<>(); Person p = new Person("张三",19); System.out.println(p.hashCode());// 24022539 set1.add(p); Person p2 = new Person("李四",19); System.out.println(p2.hashCode());// 26104871 set1.add(p2); Person p3 = new Person("张san",20); System.out.println(p3.hashCode());// 1018270485 set1.add(p3); Person p1 = new Person("张三",19); System.out.println(p1.hashCode());// 24022539 System.out.println(p.equals(p1));// true set1.add(p1); // [Person{name='张san', age=20}, Person{name='张三', age=19}, Person{name='李四', age=19}] System.out.println(set1); } }
扩展: LinkedHashSet : 类是HashSet类的子类,在保证元素唯一和功能上, 与父类HashSet完全一致, 唯一不同,LinkedHashSet底层是双向链表, 记录每一个元素前后数据的其他元素,因此效果可以保证元素迭代顺序与添加顺序一致
import java.util.HashSet; import java.util.LinkedHashSet; public class LinkedHashSetDemo { public static void main(String[] args) { HashSet<Integer> set = new HashSet<>(); set.add(15); set.add(1); set.add(99); System.out.println(set);// [1, 99, 15] LinkedHashSet<Integer> set1 = new LinkedHashSet<>(); set1.add(15); set1.add(1); set1.add(99); System.out.println(set1);// [15, 1, 99] } }
三. 双列集合
(一) Map概述
1、体系位置:双列集合的顶层接口
2、类比理解:map单词含义,地图,地图上的每个点,都表示了生活中的一个具体位置。地图的点和生活中的位置,有一个一一对应的关系,这种关系是通过穷举的方式来描述的。
3、数据结构:描述的就是一个数据(key)到另一个数据(value)的映射关系(对应关系)
4、Map<K,V>的特点:称为键值对映射关系(一对一)
Key(键)是唯一的(不重复),value(值)不是唯一的
每个键都只能对应确定唯一的值
5. Map集合没有索引, 因此存储的元素不能保证顺序
(二) Map常用的子类
通过查看Map接口描述,看到Map有多个实现类,这里我们主要讲解常用的HashMap集合 LinkedHashMap集合.
HashMap<K,V>:存储数据采用的哈希表结构,元素的存取顺序不能保证一致。由于要保证键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。
LinkedHashMap<K,V>:HashMap下有个子类LinkedHashMap,存储数据采用的哈希表结构+链表结构。通过链表结构可以保证元素的存取顺序一致;通过哈希表结构可以保证的键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。
(三) Map接口中常用的方法
1. public V put(K key, V value):
1) 如果添加的key值在map集合中不存在, 那么put方法表示将键值对添加到map集合中
2) 如果添加的key值在map集合中存在, 那么put方法表示修改value值
2. public V remove(Object key): 把指定的键所对应的键值对元素在Map集合中删除,返回被删除元素的值。
3. public V get(Object key):根据指定的键,在Map集合中获取对应的值。
4. boolean containsKey(Object key): 判断集合中是否包含指定的键。
5. void clear() : 表示清空Map集合中所有键值对数据
6. boolean isEmpty() : 验证Map集合中是否还是键值对数据, 如果没有返回true, 如果有返回false
7. int size() : 获取到Map集合中键值对数据个数(求Map集合的长度)
import java.util.HashMap; import java.util.Map; public class MapMethod { public static void main(String[] args) { Map<Integer,String> map = new HashMap<>(); // 1. public V put(K key, V value): 如果添加到Map集合中的key值不存在,put表示新增键值对数据 map.put(1,"a"); map.put(2,"b"); map.put(3,"c"); System.out.println(map);// {1=a, 2=b, 3=c} // 2. public V put(K key, V value): 如果添加到Map集合中的key值已经存在,表示value值修改(替换) map.put(2,"jack"); System.out.println(map);// {1=a, 2=jack, 3=c} // 3. HashMap实现类底层哈希表结构,因此元素存取无序; Map集合没有索引 map.put(11,"d"); map.put(6,"d"); System.out.println(map);// {1=a, 2=jack, 3=c, 6=d, 11=d} // 4. public V remove(Object key): 把指定的键所对应的键值对元素在Map集合中删除,返回被删除元素的值。 String value = map.remove(6); System.out.println(value);// d System.out.println(map);// {1=a, 2=jack, 3=c, 11=d} // 5. public V get(Object key):根据指定的键,在Map集合中获取对应的值。 System.out.println(map.get(3));// c System.out.println(map.get(99));// null // 6. boolean containsKey(Object key): 判断集合中是否包含指定的键。 System.out.println(map.containsKey(11));// true System.out.println(map.containsKey(19));// false System.out.println(map.size());// 4 // 7.void clear() : 表示清空Map集合中所有键值对数据 map.clear(); System.out.println(map);// {} // 8.boolean isEmpty() : 验证Map集合中是否还是键值对数据, 如果没有返回true, 如果有返回false System.out.println(map.isEmpty()); // true // 9.int size() : 获取到Map集合中键值对数据个数(求Map集合的长度) System.out.println(map.size()); // 0 } }
(四) Map集合的遍历方式
1.1 键遍历
1、获取Map集合中的所有键,放到一个Set集合中,遍历该Set集合,获取到每一个键,根据键再来获取对应的值。
2、获取Map集合中的所有键
Set<K> keySet()
3、遍历Set集合的两种方法:
迭代器
增强for循环
4、拿到每个键之后,获取对应的值
V get(K key)
import java.util.HashMap; import java.util.Set; public class MapKeySet { public static void main(String[] args) { HashMap<Integer,String> map = new HashMap<>(); map.put(1,"a"); map.put(2,"hello"); map.put(3,"world"); // 1. 获取到map集合中的所有key值集合 Set<Integer> set = map.keySet(); // 2. 遍历set集合,获取出每一个key值 for(Integer key : set){ // 3. 通过唯一的key值获取到对应的value值 String value = map.get(key); System.out.println(key + "--" + value); } } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!