Java学习 第十六天---泛型与集合
第一章 泛型
1.1 泛型概述
泛型产生的背景:
Java推出泛型以前,程序员可以构建一个类,但是类、接口里的方法、变量等各种东西什么的大都
必须事先定义类型,这样难免会有局限性,就产生了一个类、接口、方法只能操作某种数据类型,这显然
是不符合情理的。
当然了,有人会说,可以把这些操作的类型全部定义为Object类型,理论上这确实可以操作任意类型数
据了,可是这种多态的使用方式会产生一个问题,那就是无法使用一些数据类型本身特有的方法了,这显然
也是不符合人们需求的。
基于以上问题,是否能够给类,方法等设计一个参数来表示其操作的数据类型?在创建类的实例对象
和使用这些方法的时候再传入一个数据类型来表示其操作的数据类型到底是什么?
因此,设计为一个形式参数,比如设置为E,这样在创建集合时候给E赋值一个数据类型(实际上只能是
引用数据类型,理由在下文给出),如String、Integer。
代码演示:
ArrayList<String> list01;
ArrayList<Integer> list02;
这样我们就实现了只创建了一个类ArrayList却可以创建存储不同类型元素的对象,从而其中的方
法也就可以操作不同类型元素的对象。
当然了,这样还有一个问题就是:如上面代码创建的一样,这里的list01和list02依然还有一个问题
就是list01和list02好像还是只能分别存储String类型,Integer类型,这好像还是不太灵活。这个问题
请读者先带着继续向下看,下面会给出一定的解释
泛型的概念:
Java泛型(generics)是JDK5中引入的一个新特性,泛型提供了编译时类型安全监测机制,该
机制允许我们在检测时检测到非法的类型数据结构
泛型的本质就是参数化类型,也就是将操作的数据的数据类型设置为一个参数
示例代码如下:
public class MainClass {
public static void main(String[] args) {
// 不适用泛型创建ArrayList对象
ArrayList list = new ArrayList();
// 此时add方法接收的类型为Object类型,什么都可以添加
list.add("java");
list.add(100);
list.add(true);
// 遍历集合
for (int i = 0; i < list.size(); i++) {
Object o = list.get(i);
// 问题在于,如果要用集合中元素特有的方法,又不能统一强转
// 而如果强转这么写代码,没有发现。则强转的错误不会在编译期报错,而会在运行期报错
// 这对于开发程序是致命的
/*String str = (String) o;
System.out.println(str);*/
}
// 使用泛型创建ArrayList对象
ArrayList<String> strList = new ArrayList<>();
strList.add("a");
strList.add("b");
strList.add("c");
for (int i = 0; i < strList.size(); i++) {
String s = strList.get(i);
System.out.println(s);
}
}
}
泛型的好处:
1.类型安全
2.消除了强制类型转换 (向下转型如果有错不会在编译器出现,会在运行期出现,这是致命的)
泛型为什么只能传入引用数据类型:
泛型传入实参时只能传入引用数据类型,原因在于泛型实际上是具有一个泛型擦除的概念的,这个概念笔者暂时的知识无法理解其中的反射,getClass等方法,因此暂时不给出详细解释。简单来说就是泛型实际上在编译期是统一被编译成了Object的,只有在使用的适当时期才会将Object转换为传入的泛型实参类型,而基本数据类型int,double等是无法被编译为Object的。正是因为实际上编译后的字节码文件中不存在泛型标识,所以,泛型编写的代码才会和泛型出现以前编写的Java代码兼容性会很好的情况。
注意: 在使用泛型类、泛型方法、泛型方法时,如果不传入泛型实参,会默认将泛型当做Object来运行
1.2 泛型类
泛型类使用格式:
- 使用语法:
类名<具体的数据类型> 对象名 = new 类名<具体的数据类型>();
- Java1.7以后,后面的<>中的具体的数据类型可以省略不写
类名<具体的数据类型> 对象名 = new 类名<>();
示例代码如下:
/**
*泛型类的定义
* @param <T> 泛型标识---类型形参
* T 是创建对象时候指定的具体数据类型
*/
public class Generic<T> {
// T,是由外部使用类的时候来指定的
private T key;
public Generic(T key){
this.key = key;
};
public T getKey() {
return key;
}
public void setKey(T key) {
this.key = key;
}
@Override
public String toString() {
return "Generic{" +
"key=" + key +
'}';
}
}
// 定义测试类
public class MainClass {
public static void main(String[] args) {
// 泛型类在创建对象的时候,来指定操作的具体数据类型。
Generic<String> strGeneric = new Generic<>("abc");
String key1 = strGeneric.getKey();
System.out.println("key1:" + key1);
System.out.println("==================================");
Generic<Integer> intGeneric = new Generic<>(10);
int key2 = intGeneric.getKey();
System.out.println("key2:" + key2);
// 泛型类在创建对象的时候,没有指定类型,将按照Object类型来操作
Generic generic = new Generic("ABC");
Object key3 = generic.getKey();
System.out.println(key3);
// 泛型类型不支持基本数据类型
// 原因:在使用泛型的时候编译期,会将泛型转换为Object类型,在使用里面的成员时候会在适当时期将Object转换为传入的泛型T
// Generic<int> genericl = new Generic<>(100);
System.out.println("================================");
// 统一泛型类,根据不同的数据类型创建的对象,本质上是同一类型
System.out.println(intGeneric.getClass()); // class com.cqu.demo02.Generic
System.out.println(strGeneric.getClass()); // class com.cqu.demo02.Generic
System.out.println(intGeneric.getClass() == strGeneric.getClass()); // true
}
}
泛型类: 只定义了一个类,却可以操作不同的数据类型,增加了代码的复用性
泛型类三个注意事项:
- 泛型类,如果没有指定具体的数据类型,此时,操作的类型是Object
- 泛型的类型参数只能是引用数据类型,不能是基本数据类型
- 泛型类型在逻辑上可以看成是多个不同的类型,但实际上都是相同类型(这一条暂时不是很理解)
1.3 泛型类练习—抽奖问题
作业:
定义一个类,里面具有以下功能:
1.生成奖池,里面的奖品可以全是字符串类型的(表示物品)而,也可以全是数字(表示奖金)
2.随机抽奖功能
代码如下:
// 定义抽奖器
/**
*抽奖器
* @param <T>
*/
public class ProductGetter<T> {
static Random random = new Random();
// 奖品
private T product;
// 奖品池
ArrayList<T> list = new ArrayList<>();
// 添加奖品
public void addProduct(T t){
list.add(t);
}
// 抽奖
public T getProduct(){
product = list.get(random.nextInt(list.size()));
return product;
}
}
// 定义测试类
public class MainClass<T> {
public static void main(String[] args) {
// 创建抽奖器对象
ProductGetter<String> stringProductGetter = new ProductGetter<>();
String[] strProducts = {"苹果手机","华为手机","扫地机器人","咖啡机"};
// 给抽奖器中填充奖品
for (int i = 0; i < strProducts.length; i++) {
stringProductGetter.addProduct(strProducts[i]);
}
// 抽奖
String product1 = stringProductGetter.getProduct();
System.out.println("恭喜您,您抽中了:" + product1); // 恭喜您,您抽中了:扫地机器人
System.out.println("===============================");
ProductGetter<Integer> integerProductGetter = new ProductGetter<>();
int[] intProducts = {10000,50000,3000,50,900000};
for (int i = 0; i < intProducts.length; i++) {
integerProductGetter.addProduct(intProducts[i]);
}
Integer product2 = integerProductGetter.getProduct();
System.out.println("恭喜您,您抽中了:" + product2); // 恭喜您,您抽中了:50000
}
}
1.4 泛型类的子类
泛型类的子类只有以下两种情况:
1.子类也是泛型类,则子类的泛型标识至少有一个要和父类的泛型标识一致,如:
class ChildGeneric<K,T,M> extends Generic<T>
原因在于,在创建子类对象的时候会先创建父类对象,如果子类没有和父类一致的泛型标识,则在先创建父类对象时
无法获得创建子类对象传入的泛型类型
2.子类不是泛型类,也就是普通类则父类要明确泛型的数据类型,此时子类就相当于普通类,只是其继承的泛型父类需要传入明确泛型类型参数,如:
class ChildGeneric extends Generic<String>
原因在于,在创建子类对象的时候会先创建父类对象,如果子类不是泛型类,而父类写了泛型标识但是没有明确泛型类型,
则无法明确创建父类对象时的泛型是什么,因此要必须明确父类的泛型类型
注意:
无论是上面两种情况中的哪一种,只要不写父类泛型标识,就默认父类的泛型为Object类型,以保证创建子类对象而先创建父类对象时,泛型父类的泛型类型有值,从而可以先创建父类对象 (在堆内存new方法创建子类对象的时候会先调用父类的构造方法,先在要创建的子类对象的new方法申请的内存地址处创建一个父类对象,然后再创建子类对象)。
代码表示上述注意事项如下:
public class ChildFirst<T> extends Parent{
@Override
public Object getValue() {
// 这里super.getValue()返回值是父类的泛型,此时默认值为Object,因此重写方法的返回值类型也必须是Object类型
return super.getValue();
}
}
public class ChildSecond extends Parent{
@Override
public Object getValue() {
return super.getValue();
}
}
下面用代码演示泛型类创建子类及其使用
// 定义泛型父类
public class Parent <E>{
private E value;
public E getValue() {
return value;
}
public void setValue(E value) {
this.value = value;
}
}
// 当泛型类的子类也为泛型类时
/**
* 泛型类的子类也是泛型类,那么子类的泛型标识至少有一个要和父类的一致
* @param <T>
*/
public class ChildFirst<T> extends Parent<T>{
@Override
public T getValue() {
return super.getValue(); // 返回的是父类的泛型类型T,因此要将方法的返回值类型设置为T
}
}
// 当泛型类的子类是普通类时
/**
* 泛型类派生子类,如果子类不是泛型类型,那么父类需要明确泛型类型
*/
public class ChildSecond extends Parent<Integer>{
@Override
// 返回的是父类传入的泛型类型实参Integer,因此要将
// 方法的返回值类型设置为Integer或者Integer的父类(此时是多态)
public Integer getValue() {
return super.getValue();
}
@Override
public void setValue(Integer value){
super.setValue(value);
}
}
1.5 泛型接口
泛型接口的定义语法:
修饰符 interface 接口名称<泛型标识,泛型标识,...>{
代码块
}
泛型接口的使用:
1.实现类不是泛型类,接口要明确数据类型,此时这个实现类就是普通的实现类
2.实现类也是泛型类,实现类的泛型类型至少有一个要和接口的泛型类型要一致
注意: 如果定义泛型接口的实现类时不传入泛型接口的泛型标识,默认泛型接口中的泛型标识为Object
下面用代码演示泛型接口创建其实现类及其使用
// 定义泛型接口
public interface Generator<T> {
T getKey(); // 省略public abstract 抽象方法
}
// 泛型接口的实现类为普通类
/**
* 实现泛型接口的类,不是泛型类,需要明确实现泛型接口的数据类型,即使不写,也会默认的将其设置为Object,默认
* 实现泛型接口的数据类型
*/
public class Apple implements Generator<String>{
@Override
public String getKey() {
return "hello generic";
}
}
// 泛型接口的实现类为泛型类
/**
* 泛型接口的实现类,是一个泛型类,那么要保证实现接口的泛型类的泛型标识包含泛型接口的泛型标识
* @param <T>
* @param <E>
*/
public class Pair<T,E> implements Generator<T>{
private T key;
private E value;
public Pair(T key, E value) {
this.key = key;
this.value = value;
}
@Override
public T getKey() {
return key;
}
public E getValue() {
return value;
}
}
// 定义测试类
public class Test {
public static void main(String[] args) {
Apple apple = new Apple();
String key = apple.getKey();
System.out.println(key); // hello generic
System.out.println("----------------------------");
Pair<String,Integer> pair = new Pair<>("coung",100);
String key1 = pair.getKey();
Integer value = pair.getValue();
System.out.println("key1:" + key1); // key1:coung
System.out.println("value:" + value); // value:100
}
}
1.6 重写泛型父类或泛型接口中方法的返回值类型
假设有一个泛型父类或泛型接口Animal,里面有一个eat方法,其返回值是泛型T,那么:
- Animal的子类或实现类在重写eat方法时其返回值类型也要写为T,这是方法重写对返回值类型的约束所要求的,同理在存储返回值类型时候也要用传入的泛型实参类型来接收
- 如果在调用Animal类或Animal接口时没有传入泛型实参,也就是没给T赋值,那么这时候eat方法的返回值类型必须要用Object类型来接收,原因在于没传参数时,泛型标识T就被编译为了Object,用其他的接收会发生向下转换,这是不安全的。
1.8 泛型方法
1.8 泛型方法概述
泛型类和泛型方法对比:
- 泛型类,是在实例化类的时候指定泛型的具体类型
- 泛型方法,是在调用方法的时候指定泛型的具体类型
泛型方法特点:
1.泛型方法能使得方法独立于类而产生变化,所以真正在工作中一般使用泛型方法,而不使用泛型类,因为泛型方法更加的灵活
2.如果static方法要使用泛型能力,就必须让其成为泛型方法,这也就是说静态方法无法使用泛型类定义的泛型,原因在于如果可以使用泛型类定义的泛型标识,假设是T,那么通过类名.静态方法调用该static方法的时候并没有创建对象,而泛型类的泛型具体类型是在创建对象时候指定的,这也就意味着并没有对T进行指定,此时的T就是默认值Object,毫无意义。
泛型方法语法:
修饰符 <T,E,...> 返回值类型 方法名(参数列表){
方法体...
}
1.修饰符与返回值类型之间的<T,E...>非常重要,可以理解为声明此方法为泛型方法
2.只有声明了<T,E...>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法
3.<T>声明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T
4.与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
注意:
- 泛型类当中的普通成员方法如果使用了泛型类中的泛型标识,那么该方法不能为静态方法,
- 泛型类当中的泛型方法是可以设置为静态方法的,原因在于,泛型方法的泛型类型和泛型类的泛型类型完全不相关,就算是泛型方法和泛型类二者有相同的泛型标识(比如T),二者也毫无关系,因为泛型方法本身的特点就是可以使得方法独立于类产生变化,因此泛型方法是无法使用泛型类的泛型的。泛型方法中的泛型是在调用泛型方法时进行指定的,这和通过类名调用毫无关系
- 只有带有泛型列表的才是泛型方法,不带泛型列表的方法都是非泛型方法
泛型方法与可变参数
public <E> void print(E...e){
for(E e1:e){
System.out.println(e);
}
}
实际上,泛型就是一个引用数据类型形参而已,不难理解泛型方法也可以使用可变参数
给之前定义的抽奖器泛型类添加泛型方法和静态泛型方法
/**
*抽奖器
* @param <T>
*/
public class ProductGetter<T> {
static Random random = new Random();
// 奖品
private T product;
// 奖品池
ArrayList<T> list = new ArrayList<>();
// 添加奖品
public void addProduct(T t){
list.add(t);
}
// 抽奖
public T getProduct(){
product = list.get(random.nextInt(list.size()));
return product;
}
/**
* 定义泛型方法
* @param list 参数
* @param <T> 泛型标识,具体类型由调用方法的时候指定
* @return
*/
// 尽管泛型方法使用的泛型标识和其所在的泛型类所用泛型标识一样,但是!
// 可以验证,泛型方法中的T和泛型类中的T根本不是一个东西,二者毫无关系
// 简而言之,在创建泛型类对象时候可以 ProductGetter<Integer> productGetter = new ProductGetter<>();创建一个泛型为Integer对象
// 但是调用泛型成员方法时可以传入String类型的ArrayList ArrayList<String> strList = new ArrayList<>(); String product1 = productGetter.getProduct(strList);
public static <T> T getProduct(ArrayList<T> list){
return list.get(random.nextInt(list.size()));
}
/**
* 静态的泛型方法,采用多个泛型类型
* @param t
* @param e
* @param k
* @param <T>
* @param <E>
* @param <K>
*/
public static<T,E,K> void printType(T t,E e,K k){
System.out.println(t + "\t" + t.getClass().getSimpleName());
System.out.println(e + "\t" + e.getClass().getSimpleName());
System.out.println(k + "\t" + k.getClass().getSimpleName());
}
/**
* 泛型可变参数的定义
* @param e
* @param <E>
*/
public static <E> void print(E... e){
for (int i = 0; i < e.length; i++) {
System.out.println(e[i]);
}
}
}
1.9 泛型通配符和泛型约束
1.9.1 泛型通配符
牢记一句话,泛型没有继承概念,泛型没有继承概念,泛型没有继承概念!
简单理解一下下面代码:
ArrayList<Object> list01 = new ArrayList<>;
ArrayList<Integer> list02 = new ArrayList<>;
这里的list01并不是list02的父类,二者并没有继承关系,原因在于ArrayList是个泛型类,泛型类的实例对象可以定义泛型实参为很多数据类型,但是这些对象本身都还是ArrayList这个类,他们并没有逃脱出ArrayList这个范围,这也就是泛型类的第三个特点
看下列代码
// 定义一个泛型类Box
public class Box <E>{
private E first;
public E getFirst() {
return first;
}
public void setFirst(E first) {
this.first = first;
}
}
// 定义测试类
public class Test {
public static void main(String[] args) {
Box<Number> box1 = new Box<>();
box1.setFirst(100);
showBox(box1);
Box<Integer> box2 = new Box<>();
box2.setFirst(200);
// 这里代码会报错,原因在于showBox方法要求传入的必须是存储Number类型的Box容器
// 但是下面代码传入的确是存储Integer类型的Box容器
// 由于泛型是没有继承概念的,所以这里会报错
// 同样的,你即使把showBox方法的传入参数设置为Box<Object> box也同样不对
// 原因依然在于泛型没有继承这么一个说法,你设置的为Box<Object> box就必须传入存储Object对象的Box容器
showBox(box2);
}
// 如果将参数的泛型类的泛型设置为Object,想让其接收任意类型的Box,也是同样的错误,原因依然是泛型不具有继承关系
// 如果想传入的参数为任意类型的泛型类Box的对象,那么需要使用泛型通配符?来代替泛型的实参(如Integer,String这些实参),以此来
// 代表这里的泛型可以为任意类型
public static void showBox(Box<Number> box){
// 因为泛型?代表任意具体的引用数据类型,所以这里只能用Object来进行接收
Object first = box.getFirst();
System.out.println(first);
}
修改上述代码,并回顾泛型类的第三个特点
public class Test {
public static void main(String[] args) {
Box<Number> box1 = new Box<>();
box1.setFirst(100);
showBox(box1);
Box<Integer> box2 = new Box<>();
box2.setFirst(200);
showBox(box2);
}
// 给泛型通配符加上上限Number
public static void showBox(Box<?> box){
// 因为泛型?代表任意具体的引用数据类型,所以这里只能用Object来进行接收
Number first = box.getFirst();
System.out.println(first);
}
// 回顾泛型类的第三个特性,虽然泛型类的泛型会有所不同,但是本质上他们都是同一个类型,也就是它们的类类型,就是Box
// 因此这两个重名的方法并不是重载
/*public static void showBox(Box<Integer> box){
Number first = box.getFirst();
System.out.println(first);
}*/
}
泛型通配符:
- 类型通配符一般是使用?代替具体的类型 “实参”,他可以代表任意具体的引用数据类型
- 所以,类型通配符是类型实参,而不是类型形参
简而言之,就是把?看做一个具体的引用数据类型而已。
泛型通配符的缺陷: 在上述代码中将泛型标识改为?,则泛型可以传入任意引用数据类型,这没法统一其定义的类型,从而无法对传入的泛型实参定义的变量使用一些类或接口才有的方法,因此要对泛型类型加一些约束,从而引发了泛型约束
1.9.2 上限通配符
1.9.2.1 上限通配符的简单使用
上限通配符使用格式:
<? extends 类或接口>
意思是这里传入的泛型类型只能是extends后的类或接口(假设为Cat)本身或其子类。
定义三个类,其中MiniCat继承Cat,Cat继承Animal
// 定义最高类Animal
public class Animal {
public String name;
public Animal(String name) {
this.name = name;
}
@Override
public String toString() {
return "Animal{" +
"name='" + name + '\'' +
'}';
}
}
// 定义Animal子类Cat
public class Cat extends Animal{
public int age;
public Cat(String name, int age) {
super(name);
this.age = age;
}
@Override
public String toString() {
return "Cat{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
// 定义Cat子类MiniCat
public class MiniCat extends Cat{
public int level;
public MiniCat(String name, int age, int level) {
super(name, age);
this.level = level;
}
@Override
public String toString() {
return "MiniCat{" +
"level=" + level +
", age=" + age +
", name='" + name + '\'' +
'}';
}
}
因为List接口中的addAll方法源码 public boolean addAll(Collection<? extends E> c)使用了上限通配符,所以运用ArrayList和List接口中的addAll方法进行泛型上限通配符演示,代码如下:
public class TestUp {
public static void main(String[] args) {
ArrayList<Animal> animals = new ArrayList<>();
ArrayList<Cat> cats = new ArrayList<>();
ArrayList<MiniCat> miniCats = new ArrayList<>();
// List接口中的addAll方法源码 public boolean addAll(Collection<? extends E> c) {方法体}
// 可以看出,addAll要求传入的参数是一个具有泛型上限通配符的Collection集合
// 注意:1.源码中的E就是创建List对象这个泛型类时候的E,这里就是cats定义时的泛型类型Cat
// 2.子类对象也是父类对象,所以也可以传入的是ArrayList集合,相当于发生了向上转型,安全
/*cats.addAll(animals);*/
cats.addAll(cats);
cats.addAll(miniCats);
// showAnimal(animals); // 传递的泛型实参只能是Cat及其子类
showAnimal(cats);
showAnimal(miniCats);
}
public static void showAnimal(ArrayList<? extends Cat> list){
}
}
1.9.2.1 上限通配符创建的容器注意事项
上限通配符创建的容器,取出元素无影响,但是要用上限类型进行接收:
不难理解,使用上限通配符创建的容器在取出元素时,由于不确定传入的泛型类型具体是什么,只知道这个泛型的上界,所以为了安全,只能用这个上限进行接收(当然也可以用这个上限的上限比如Object接收),这时可能是多态接收
public class TestUp {
public static void main(String[] args) {
ArrayList<Animal> animals = new ArrayList<>();
ArrayList<Cat> cats = new ArrayList<>();
ArrayList<MiniCat> miniCats = new ArrayList<>();
// List接口中的addAll方法源码 public boolean addAll(Collection<? extends E> c) {方法体}
// 可以看出,addAll要求传入的参数是一个具有泛型上限通配符的Collection集合
// 注意:1.源码中的E就是创建List对象这个泛型类时候的E
// 2.子类对象也是父类对象,所以也可以传入的是ArrayList集合
/*cats.addAll(animals);*/
cats.addAll(cats);
cats.addAll(miniCats);
// showAnimal(animals); // 传递的泛型实参只能是Cat及其子类
showAnimal(cats);
showAnimal(miniCats);
}
public static void showAnimal(ArrayList<? extends Cat> list){
for (int i = 0; i < list.size(); i++) {
// 这可能是一个多态写法
Cat cat = list.get(i);
System.out.println(cat);
}
}
}
使用上限通配符创建的容器,无法添加除了null值之外的任何元素
public class TestUp {
public static void main(String[] args) {
ArrayList<Animal> animals = new ArrayList<>();
ArrayList<Cat> cats = new ArrayList<>();
ArrayList<MiniCat> miniCats = new ArrayList<>();
// List接口中的addAll方法源码 public boolean addAll(Collection<? extends E> c) {方法体}
// 可以看出,addAll要求传入的参数是一个具有泛型上限通配符的Collection集合
// 注意:1.源码中的E就是创建List对象这个泛型类时候的E
// 2.子类对象也是父类对象,所以也可以传入的是ArrayList集合
/*cats.addAll(animals);*/
cats.addAll(cats);
cats.addAll(miniCats);
// showAnimal(animals); // 传递的泛型实参只能是Cat及其子类
showAnimal(cats);
showAnimal(miniCats);
}
/**
* 泛型上限通配符,传递的集合类型,只能是Cat或Cat的子类类型
* @param list
*/
public static void showAnimal(ArrayList<? extends Cat> list){
*/
/*list.add(new Animal());
list.add(new Cat());
list.add(new MiniCat());*/
for (int i = 0; i < list.size(); i++) {
// 这可能是一个多态写法
Cat cat = list.get(i);
System.out.println(cat);
}
}
}
上述代码中下面三行代码是错误的,所以要注释,并在下面代码块中进行分析解释为什么无法添加除了null之外的任何元素
/*list.add(new Animal());
list.add(new Cat());
list.add(new MiniCat());*/
首先,上述三行代码中的list是自定义的public static void showAnimal(ArrayList<? extends Cat> list)方法
中设置的参数列表中使用上限通配符定义的list,这里的类型上限是Cat,所以在调用此方法时根据传入的参数可以判断
上述三行代码中的list在定义时的存储类型只可能是Cat和它的子类MiniCat。
分析,当list定义的存储类型是Cat时,上述第一行代码存储的是一个Animal对象,因此在存储的时候会将Animal
向下转型成Cat对象,这是不安全的,所以此时第一行代码错误,也就是不能添加Animal对象
当list定义的存储类型是MiniCat时,,上述第一、二行代码存储的分别是一个Animal和Cat对象,因此在存储的时候
会将Animal和Cat对象向下转型成Cat对象,这是不安全的,所以此时第一、二行代码错误,也就是不能添加Animal和
Cat对象
此时有人会说,根据上述分析我完全可以只写第三行代码MiniCat啊,上面的分析也没有说出为什么第三行代码报错啊。
表面上如此,但是实际上我们定义的参数list的存储类型是一个有类型上限的ArrayList集合,这个上限是Cat,但是Cat的子
类可能有很多个啊,可能不止一个MiniCat。加入Cat还有一个子类叫做BlackCat,那么如果上述三行代码中的list对象在
定义时定义其存储的是BlackCat类型,那么是不是上面的第三行代码在存储MiniCat的时候会发生类型转换异常?
至于在存储null值的时候,无论上述三行代码中的list定义的存储类型是什么,,都不会发生类型转换异常,因此可以
进行存储,
综上所述,使用上限通配符创建的容器,无法添加除了null值之外的任何元素
1.9.3 下限通配符
1.9.3.1 下限通配符的简单使用
下限通配符使用格式:
<? super类或接口>
意思是这里传入的泛型类型只能是extends后的类或接口(假设为Cat)本身或其父类。
依然使用上面定义的三个类,其中MiniCat继承Cat,Cat继承Animal
// 定义最高类Animal
public class Animal {
public String name;
public Animal(String name) {
this.name = name;
}
@Override
public String toString() {
return "Animal{" +
"name='" + name + '\'' +
'}';
}
}
// 定义Animal子类Cat
public class Cat extends Animal{
public int age;
public Cat(String name, int age) {
super(name);
this.age = age;
}
@Override
public String toString() {
return "Cat{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
// 定义Cat子类MiniCat
public class MiniCat extends Cat{
public int level;
public MiniCat(String name, int age, int level) {
super(name, age);
this.level = level;
}
@Override
public String toString() {
return "MiniCat{" +
"level=" + level +
", age=" + age +
", name='" + name + '\'' +
'}';
}
}
public class TestDown {
public static void main(String[] args) {
ArrayList<Animal> animals = new ArrayList<>();
ArrayList<Cat> cats = new ArrayList<>();
ArrayList<MiniCat> miniCats = new ArrayList<>();
showAnimal(animals);
showAnimal(cats);
// showAnimal(miniCats);
System.out.println("====================================");
}
/**
* 类型通配符下限,要求集合只能是Cat或Cat的父类类型
* @param list
*/
public static void showAnimal(List<? super Cat> list){
}
}
1.9.3.2 treeSet中比较器的巧妙解读
public class Test08 {
public static void main(String[] args) {
/*
TreeSet有一个构造方法,其参数为使用下限通配符的比较器Comparator:
public TreeSet(Comparator<? super E> comparator) {}
实际上之所以使用下限通配符的比较器也是很合理的,因为创建子类对象的时候会先初始化父类对象,
其父类的成员已经被初始化
此时,如果传入的比较器用E为下限的,那么依然可以按照比较器中的compare方法,从而根据父类的相关
属性对TreeSet集合中的元素进行比较排序
但反之,创建对象(Cat)的时候不能创建其子类对象(MiniCat),所以如果传入以E的子类为泛型的
比较器comparator的话,其中的比较方法compare是用子类对象(MiniCat)的相关成员进行比较的,但是
TreeSet集合中的对象(Cat类型)是没有这些子类对象(MiniCat)所具有的属性的,因此无法
利用这样的比较器进行比较
综上所述,TreeSet集合的有参构造方法中的比较器设计为使用下限通配符的比较器Comparator是十分巧妙并且合理的
*/
// 此时的E就是实例化对象treeSet时传入的Cat,右边传入的泛型是Comparator1的泛型Animal,为
// Cat的父类,满足要求,程序运行正常
TreeSet<Cat> treeSet = new TreeSet<>(new Comparator1());
// 此时的E就是实例化对象treeSet时传入的Cat,右边传入的泛型是Comparator2的泛型Cat,为
// Cat的父类,满足要求,程序运行正常
// TreeSet<Cat> treeSet = new TreeSet<>(new Comparator2());
// 此时的E就是实例化对象treeSet时传入的Cat,右边传入的泛型是Comparator3的泛型MiniCat,不满足
// 下限通配符的要求,报错
// TreeSet<Cat> treeSet = new TreeSet<>(new Comparator3());
treeSet.add(new Cat("jerry",20));
treeSet.add(new Cat("amy",22));
treeSet.add(new Cat("frank",25));
treeSet.add(new Cat("jim",15));
for (Cat cat : treeSet) {
System.out.println(cat);
}
}
}
class Comparator1 implements Comparator<Animal> {
@Override
public int compare(Animal o1, Animal o2) {
return o1.name.compareTo(o2.name);
}
}
class Comparator2 implements Comparator<Cat>{
@Override
public int compare(Cat o1, Cat o2) {
return o1.age - o2.age;
}
}
class Comparator3 implements Comparator<MiniCat>{
@Override
public int compare(MiniCat o1, MiniCat o2) {
return o1.level - o2.level;
}
}
1.9.3.3 下限通配符创建的容器注意事项
下限通配符创建的容器,取出元素有限制,只能用Object进行接收:
不难理解,使用下限通配符创建的容器在取出元素时,由于不确定传入的泛型类型具体是什么,只知道这个泛型的下界,也就是说可以传入这个高于这个下界的所有引用数据类型,所以为了避免发生向下转型,只能用Object这个最高级父类进行多态接收。
下限通配符创建的容器,可以添加元素,但是也有限制:
public class TestDown {
public static void main(String[] args) {
ArrayList<Animal> animals = new ArrayList<>();
ArrayList<Cat> cats = new ArrayList<>();
ArrayList<MiniCat> miniCats = new ArrayList<>();
showAnimal(animals);
showAnimal(cats);
// showAnimal(miniCats);
System.out.println("====================================");
}
/**
* 类型通配符下限,要求集合只能是Cat或Cat的父类类型
* @param list
*/
public static void showAnimal(List<? super Cat> list){
// 使用下限通配符创建的容器可以添加元素,可添加的元素类型为通配符中的下限类型及其子类
// 原因在于,虽然这样无法保证容器存储的数据类型保持一致,但是知道Cat是下界,list存储的类型只能
// 是Cat或Cat的父类
// 因此在存入Cat和其子类的时候,只会发生向上转型为list的存储类型,也就是转型为Cat或Cat的父类
// 而向上转型是安全的,所以可以添加Cat及其子类
list.add(new Cat("橘猫",18));
list.add(new MiniCat("小橘猫",10,8));
for (Object o : list) {
System.out.println(o);
}
}
}
1.10 集合添加元素和接收元素时的注意事项
定义一个容器存储某一类型元素实际上是可以存储多类型的,但是存储元素的时候一定是按统一类型进行存储编译的,一旦存储元素类型和容器定义的存储类型不一致,则可能在存储时发生转型,从而报错
可以通过下面代码进行验证:
ArrayList<Number> list = new ArrayList<>();
// list定义存储类型时Number类型数据,因此可以添加Number的子类对象也就是Integer对象,在
// 存储时向上转型为Number,是安全的
list.add(new Integer(10));
// list定义存储类型时Number类型数据,因此可以添加Number的子类对象也就是Double对象,在存储时向上转型
// 为Number,是安全的
list.add(new Double(11.5));
// 采用Number进行多态接收,没问题
Number number = list.get(0);
// int number2 = list.get(0);
// 采用原本的类型int接收,出现报错 java: 不兼容的类型: java.lang.Number无法转换为int
// 由此可知,list真的是可以添加其定义存储的类型Number及其子类,但是存储时一定将其转型成了本身定义时候的存储类型
// 然而一旦存储了定义的存储类型和其子类,那么就只能用这些存储元素中最大的类型上限那个类型(也就是定义存储的类型)来接收变量
// 否则会发生向下转型,从而引发类型转换异常
// 虽然在存储的时候只能用上限类型(定义的存储类型)来接收变量,但是依然可以通过强制下行转换为其本来的类型
System.out.println(number instanceof Integer); // true
Integer numberNew = (Integer) number;
System.out.println(numberNew); // 10
理解了上述原理,那么下面代码也就不难理解了
Number[] in = {10,15.5}; // 可以存储定义存储类型Number的子类Integer和Double
Number number1 = in[0]; // 取的时候要按照定义的存储类型来存储取出的元素
int number = in[0]; // java: 不兼容的类型: java.lang.Number无法转换为int
这么写可能又会让人误以为泛型具有继承性,下面说明和所谓的泛型继承的区别
public static void method1(ArrayList<Object> list){
}
// method1方法中传入的实参一定是定义存储Object类型的ArrayList集合或定义存储Object类型的ArrayList集合的子类
// 也就是说ArrayList<Object> list1和ArrayList<Integer> list02这里的list01并不是list02的父类,这不是继承关系
// list01和list02本质上都是ArrayList集合的实例对象,这也是泛型类的第三个特点,牢记这个例子,他们不是继承关系
// ArrayList集合中重写的add方法
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
// 这里的add方法要求传入的是泛型E类型的实参,实际上如果定义泛型E的实参是Number,那么依然可以传入它的子类Integer和Double
第二章 集合
2.1 集合概述
集合框架图借鉴自博主Happytoo_
参考博主浪子回头
在第十五天笔记中已经学习过Collection接口的7个常用方法:
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(): 把集合中的元素,存储到数组中。
按照上述框架首先学习Collection接口下的集合类型。
2.2 List接口
Collection接口定义的7个方法已经学过,不再赘述,针对Collection接口只需要掌握笔记2.1节的逻辑关系图和7个方法,以及Collection是单列集合类即可。
List接口有三大特点(有序、有索引、可重复):
1.有索引
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):用指定元素替换集合中指定位置的元素,返回值的更新前的元素。
使用代码演示如下:
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); // 重写了toString方法
// public void add(int index,E element):将指定的元素,添加到该集合中的指定位置上。
// 在c和d之间添加cqu
list.add(3,"cqu");
System.out.println(list); // [a, b, c, cqu, d, a]
// public E remove(int index): 移除列表中指定位置的元素, 返回的是被移除的元素。
// 移除元素
String removeE = list.remove(4);
System.out.println("被移除的元素:" + removeE); // 被移除的元素:d
System.out.println(list); // [a, b, c, cqu, a]
// public E set(int index, E element):用指定元素替换集合中指定位置的元素,返回值的更新前该位置的元素。
// 把最后一个a,替换为A
String str = list.set(4,"A");
System.out.println("被替换的元素:" + str); // 被替换的元素:a
System.out.println(list); // [a, b, c, cqu, A]
// list集合遍历有3种方式
// 使用普通的for循环
for(int i = 0; i < list.size();i++){
System.out.println(list.get(i));
}
System.out.println("==================");
// 使用迭代器遍历
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
String s = iterator.next();
System.out.println(s);
}
System.out.println("====================");
// 使用迭代器的简化版本,也就是增强for循环
for(String s : list){
System.out.println(s);
}
}
}
2.2.1 LinkedList集合
LinkedList集合的两个特点:
- 底层是一个双向链表结构,而ArrayList底层是一个byte数组,因此
LinkedList具有查询慢,增删块的特点(和ArrayList刚好相反)。而ArrayList是查询快(连续地址值)
增删慢 - 由于LinkedList底层是双向链表结构,所以虽然其查询慢,增删快,但是
找到首位元素的速度还是很快的,因此里边包含了大量操作首尾元素的方法
注意:使用Linkedlist集合特有的方法不能使用多态,不难理解,因为这是多态的一个弊端,也就是多态对象不能使用子类特点有的方法
LinkedList集合的三对,共六个方法
// 两个添加的方法
public void addFirst(E e):将指定元素插入此列表的开头。
public void addLast(E e):将指定元素添加到此列表的结尾。 此方法等效于add(E e)
// 两个获取的方法
public E getFirst():返回此列表的第一个元素。
public E getLast():返回此列表的最后一个元素。
// 两个删除的方法
public E removeFirst():移除并返回此列表的第一个元素。
public E removeLast():移除并返回此列表的最后一个元素。
实际上还有两个方法【见到要认识,非重点】:
public void push(E e):将元素推入此列表所表示的堆栈。 // 该方法等价于addFirst,所以不用掌握,了解即可。
public E pop():从此列表所表示的堆栈处弹出一个元素。 // 此方法相当于removeFirst(),所以不用掌握,了解即可。
使用代码演示如下;
public class Demo02LinkedList {
public static void main(String[] args) {
show01();
show02();
show03();
}
/*
* `public void addFirst(E e):将指定元素插入此列表的开头。
* `public void addLast(E e):将指定元素添加到此列表的结尾。
* `public void push(E e):将元素推入此列表所表示的堆栈。
*/
public static void show01(){
// 创建LinkedList集合对象
LinkedList<String> linked = new LinkedList<>();
// 使用add方法向集合中添加元素
linked.add("泰隆");
linked.add("古拉加斯");
linked.add("艾瑞莉娅");
linked.add(null);
System.out.println(linked); // [泰隆, 古拉加斯, 艾瑞莉娅, null]
// public void addFirst(E e):将指定元素插入此列表的开头。
// linked.addFirst("www");
// System.out.println(linked); // [www, 泰隆, 古拉加斯, 艾瑞莉娅, null]
// public void push(E e):将元素推入此列表所表示的堆栈。 此方法等效于addFirst
linked.push("www");
System.out.println(linked); // [www, 泰隆, 古拉加斯, 艾瑞莉娅, null]
// public void addLast(E e):将指定元素添加到此列表的结尾。此方法等效于 add(E e)
linked.addLast("com");
System.out.println(linked); // [www, 泰隆, 古拉加斯, 艾瑞莉娅, null, com]
}
public static void show02(){
// 创建LinkedList集合对象
LinkedList<String> linked = new LinkedList<>();
// 使用add方法向集合中添加元素
linked.add("泰隆");
linked.add("古拉加斯");
linked.add("艾瑞莉娅");
linked.add(null);
System.out.println(linked);
// linked.clear(); // 再获取集合中的元素,会抛出NoSuchElementException异常
// public boolean isEmpty():如果列表不包含元素,则返回true。
// 使用isEmpty()方法判断,增加代码安全性
if(!linked.isEmpty()){
/*
public E getFirst():返回此列表的第一个元素。
public E getLast():返回此列表的最后一个元素。
*/
String first = linked.getFirst();
System.out.println(first); // 泰隆
String last = linked.getLast();
System.out.println(last); // 艾瑞莉娅
}
}
/*
public E removeFirst():移除并返回此列表的第一个元素。
public E removeLast():移除并返回此列表的最后一个元素。
public E pop():从此列表所表示的堆栈处弹出一个元素。此方法相当于removeFirst()
*/
public static void show03(){
// 创建LinkedList集合对象
LinkedList<String> linked = new LinkedList<>();
// 使用add方法向集合中添加元素
linked.add("泰隆");
linked.add("古拉加斯");
linked.add("艾瑞莉娅");
linked.add(null);
System.out.println(linked); // [泰隆, 古拉加斯, 艾瑞莉娅, null]
/*String first = linked.removeFirst();
System.out.println("被移除的第一个元素:" + first); // 被移除的第一个元素:泰隆*/
String first = linked.pop();
System.out.println("被移除的第一个元素:" + first); // 被移除的第一个元素:泰隆
String last = linked.removeLast();
System.out.println("被移除的最后一个元素:" + last); // 被移除的最后一个元素:null
}
}
2.3 Set接口
Set接口有两个特点(无索引、无重复):
- 没有索引,没有带索引的方法,也不能使用普通for循环遍历
- 由于没有索引,所以不允许存储重复的元素
- HashSet存入和取出的顺序不同,LinkedHashSet存入和取出的顺序相同,treeSet存入的数据是按照比较器和自然比较规则排序了的
使用下面代码可以测试出Set集合的上述两个特点:
public class Demo01Set {
public static void main(String[] args) {
// 多态写法
Set<Integer> set = new HashSet<>();
// 使用add方法向集合中添加元素
set.add(1);
set.add(3);
set.add(2);
set.add(1);
// 使用迭代器遍历set集合
Iterator<Integer> ite = set.iterator();
while(ite.hasNext()){
int i = ite.next(); // 发生了自动拆箱
System.out.println(i); // 输出的顺序和存储的顺序不一致,并且只输出了一个1,也就是不允许存储相同元素
}
System.out.println("=======================");
// 使用增强for遍历
for (Integer integer : set) {
System.out.println(integer);
}
}
}
2.3.1 hashCode方法和哈希码值
2.3.1.1 概述
哈希值:
哈希值是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值
注意:
1.哈希值不是对象的地址值
2.哈希值是一个int类型的数值
Object类中有一个hashCode方法可以获取对象的哈希码值
hashCode方法格式:
public int hashCode(): // 返回对象的哈希码值
由于在重写方法的时候对返回值类型有一定的要求,所以重写hashCode方法的返回值也一定是int类型
2.3.1.2 哈希码值与对象实际内存地址值的关系
首先,hashcode方法返回的哈希码值是根据对象的内存地址经哈希算法得来的。
因此,如果对象的实际内存地址值是相同的,那么根据相同的实际内存地址之和哈希算法算出的它们的哈希码值当然应该一样。
但是,根据对象的内存地址经哈希算法得来的哈希码值相同并不能保证两个对象的实际内存地址值也一样。这就好比一个普通函数f(x),f(a) = f(b),并不能保证a = b,除非这个函数是一个单射。
因此,我们把哈希算法想象成一个非单射的映射,就能得到以下结论
两个对象相等,hashcode一定相等
两个对象不等,hashcode不一定不等
hashcode相等,两个对象不一定相等
hashcode不等,两个对象一定不等
2.3.1.3 重写equals方法为什么也要重写hashCode方法
之前已经学过equals方法用于判断两个元素是否相等,假设我们重写equals方法定义一个Person类,而不重写hashCode方法
代码演示如下:
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);
}
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;
}
}
定义测试类如下:
public class Demo03HashSetSavePerson {
public static void main(String[] args) {
// 创建HashSet集合存储Person
HashSet<Person> set = new HashSet<>();
Person p1 = new Person("泰隆",18);
Person p2 = new Person("泰隆",18);
Person p3 = new Person("泰隆",19);
System.out.println(p1.hashCode()); // 1967205423
System.out.println(p2.hashCode()); // 42121758
System.out.println(p1 == p2); // false
System.out.println(p1.equals(p2)); // true
set.add(p1);
set.add(p2);
set.add(p3);
System.out.println(set.toString());
}
}
测试类中出现了一个矛盾,p1和p2内容相同,按照重写的equals方法判断p1和p2应该是同一个人,但是他们的哈希码值却不一样。这就产生了问题!我们上面说的对象如果相同,他们的哈希码值一定相同,因此我们在重写equals方法的同时必须重写hashCode方法以保证符合哈希码值的特点。
2.3.2 Set集合保证元素不重复原理分析
下面两张图虽然说是HashSet,实际上也是Set接口如何判断两个元素是重复元素的原理。
2.3.3 HashSet集合
HashSet的特点:
- 是一个无序的集合,存储和取出元素的顺序可能不一致
- 底层是一个哈希表结构(查询速度非常快)
哈希表的底层实现:
- JDK8之前,哈希表底层采用数组+链表实现,可以说是一个元素为链表的数组
- JDK8以后,在比较大的时候,底层实现了优化,变成了数组+链表+红黑树实现
2.3.4 LinkedHashSet集合
LinkedHashSet是HashSet的子类,其底层是哈希表(数组 + 链表 / 红黑树) + 链表,相比于父类HashSet而言,LinkedHashSet最大的特点就是多了层链表,而这条链表就记录着元素的存入顺序,因此LinkedHashSet是有序的
可以通过下列代码验证LinkedHashSet的有序这一特点
public class Demo04LinkedHashSet {
public static void main(String[] args) {
HashSet<String> set = new HashSet<>();
set.add("www");
set.add("abc");
set.add("abc");
set.add("cqu");
System.out.println(set); // [abc, www, cqu]无序,不允许重复
LinkedHashSet<String> linked = new LinkedHashSet<>();
linked.add("www");
linked.add("abc");
linked.add("abc");
linked.add("cqu");
System.out.println(linked); // [www, abc, cqu]有序,不允许重复
}
}
2.3.5 TreeSet集合
TreeSet集合的特点:
- 元素有序,这里的顺序不是指存储和取出的顺序,而是按照一定的规则进行排序,具体排序方式取决于构造方法
- TreeSet():根据其元素的自然排序(比如说存储int数字时候,按照int数字大小进行升序排序)进行排序
- TreeSet(Comparator comparator):根据指定的比较器进行排序
2.3.5.1 Comparable比较接口
Comparable: 强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的compareTo方法被称为它的自然比较方法。只能在类中实现compareTo()一次,不能经常修改类的代码实现自己想要的排序。
简单理解就是如果一个类实现了Comparable这个接口,并重写了接口中的compareTo方法,那么这个实现类就具有了大小关系,其比较规则就是compareTo方法定义的规则。
Comparable接口实现比较功能的缺点:
按照编写代码不易经常修改的原则,我们不能想改变其实现类对象的比较大小规则,就去修改重写的compareTo方法,这很不方便也不安全。
至于如何定义比较规则比较简单,简单来说就是如果a.compareTo(b)返回正数,则认为a>b;返回负数,认为a<b;返回0,则认为a == b,此时认为这是重复元素,Set集合是不能同时存储a和b这两个元素的
基于以上Comparable接口的缺点,提出了Comparator比较器这一接口,来实现对某一对象进行整体排序。
2.3.5.2 Comparator比较接口
Comparator: 强行对某个对象进行整体排序。
简单理解就是,一个Comparator接口的实现类的一个对象(必须重写其中的compare方法以定义比较规则)就是一个比较规则,我们可以将这个对象定义为一个比较器,这个比较器代表着一种比较大小的规则
注意: 由于比较器很灵活,且往往只是在需要修改比较规则时用,不会长期、多次使用,因此常用匿名内部类 + 匿名对象的方式来创建比较器。
compare方法定义比较规则和compareTo方法几乎完全一样,compare(a,b)如果返回正数,则认为a>b;返回负数,认为a<b;返回0,则认为a == b,此时认为这是重复元素,Set集合是不能同时存储a和b这两个元素的
2.3.5.3 TreeSet的使用
TreeSet集合是有两种构造方法的,一种是不传入比较器,按照自然排序(调用元素重写的compareTo方法,如果没有重写,则无法存入集合),另外一种是传入比较器,按照比较器规则排序
题目: 存储学生对象并且遍历,创建TreeSet集合使用带参构造方法
要求: 按照年龄从小到大排序,年龄相同时,按照姓名的字母顺序排序
// 定义学生类
public class Student implements Comparable<Student>{
private String name;
private int age;
public Student() {
}
public Student(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;
}
}
// 定义测试类
public class TreeSetDemo{
public static void main(String[] args){
// 创建集合类对象
TreeSet<Student> ts = new TreeSet<Student>(new Comparator<Student>){
@Override
public int compare(Student o1,Student o2){
int num = s1.getAge() - s2.getAge();
int num2 = num == 0 ? s1.getName().compareTo(s2.getName()) : num;
}
});
// 创建集合对象
TreeSet<Student> ts = new TreeSet<>();
// 创建学生对象
Student s1 = new Student("xishi",18);
Student s2 = new Student("diaochan",19);
Student s3 = new Student("yangyuhuan",21);
Student s4 = new Student("wangzhaojun",25);
Student s5 = new Student("linqingxia",18);
Student s6 = new Student("linqingxia",18);
// 把学生添加到集合ts
ts.add(s1);
ts.add(s2);
ts.add(s3);
ts.add(s4);
// 遍历集合
for (Student t : ts) {
System.out.println(t.getName() + "," + t.getAge());
}
}
}
结论:
- 用TreeSet集合存储自定义对象,带参构造方法使用的是比较器排序对元素进行排序的
- 比较器排序,就是让集合构造方法接收Comparator的实现类对象,重写compare(T o1 , T o2)方法
- 重写方法时,一定要注意排序规则必须按照要求的主要条件和次要条件来写
- 比较器中compare方法和Comparable接口中要重写的compareTo方法一样,如果返回值为0,则会默认两个元素相同,在Set,Map不允许存储重复元素的集合中是不会存储这种元素的
2.4 Collections工具类
该工具类有四个成员方法addAll操做对象是Collection,shuffle和sort操作的都是List
public static <T> boolean addAll(Collection<T> c, T... elements) :往集合中添加一些元素。
public static void shuffle(List<?> list) 打乱顺序`:打乱集合顺序。
public static <T> void sort(List<T> list):将集合中元素按照默认规则排序。
public static <T> void sort(List<T> list,Comparator<? super T> ):将集合中元素按照指定规则排序。
public static <T> boolean addAll(Collection<T> c, T... elements)
:往集合中添加一些元素。
代码演示如下:
public class Demo01Collections {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
/*list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("e");
System.out.println(list); // [a, b, c, d, e]*/
// public static <T> boolean addAll(Collection<T> c, T... elements) :往集合中添加一些元素。
Collections.addAll(list,"a","b","c","d","e");
System.out.println(list); // [a, b, c, d, e]*/
}
}
public static void shuffle(List<?> list) 打乱顺序
:打乱集合顺序` :往集合中添加一些元素。
注意: 这里只能传入List接口下的集合
代码演示如下;
public class Demo01Collections {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
/*list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("e");
System.out.println(list); // [a, b, c, d, e]*/
// public static <T> boolean addAll(Collection<T> c, T... elements) :往集合中添加一些元素。
Collections.addAll(list,"a","b","c","d","e");
System.out.println(list); // [a, b, c, d, e]*/
// public static void shuffle(List<?> list) 打乱顺序:打乱集合顺序。
Collections.shuffle(list);
System.out.println(list); // [c, e, b, d, a] [a, b, c, d, e]
}
}
public static <T> void sort(List<T> list)
:将集合中元素按照默认规则排序。
注意:
这里只能传入List接口下的集合
sort(list list)方法使用时,被排序的集合里边存储的元素,必须实现Comparable,重写接口中的方法compareTo定义排序的规则
// 定义Person类
public class Person implements Comparable<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 int compareTo(Person o){
// return 0; // 认为元素都是相同的
// 自定义比较的规则,比较两个人的年龄(this,参数Person)
return this.getAge() - o.getAge(); // 年龄升序排序
}
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;
}
}
//定义测试类
public class Demo02Sort {
public static void main(String[] args) {
ArrayList<Integer> list01 = new ArrayList<>();
// public static <T> boolean addAll(Collection<T> c, T... elements) :往集合中添加一些元素。
Collections.addAll(list01,1,3,2);
System.out.println("排序前:" + list01);
// public static <T> void sort(List<T> list):将集合中元素按照默认规则排序。
// 注意该方法传入的参数2不是Collection形式,只能是List形式,不能是Set形式
Collections.sort(list01); // 默认是升序
System.out.println("排序后:" + list01); // [1,2,3]
ArrayList<String> list02 = new ArrayList<>();
Collections.addAll(list02,"a","c","b");
System.out.println("排序前:" + list02); // [a, c, b]
Collections.sort(list02);
System.out.println("排序后:" + list02); // 排序后:[a, b, c]
ArrayList<Person> list03 = new ArrayList<>();
list03.add(new Person("泰隆",18));
list03.add(new Person("艾瑞莉娅",19));
list03.add(new Person("卡兹克",17));
System.out.println("排序前:" + list03); // 排序前:[Person{name='泰隆', age=18}, Person{name='艾瑞莉娅', age=19}, Person{name='卡兹克', age=17}]
// Collections.sort(list03); // 报错
// 让Person类实现Comparable接口,重写compareTo方法
Collections.sort(list03);
System.out.println("排序后:" + list03);
}
}
public static <T> void sort(List<T> list,Comparator<? super T> )
:将集合中元素按照指定规则排序。
注意: 这里只能传入List接口下的集合
// 定义Student类
public class Student {
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", 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;
}
}
// 定义测试类
public class Demo03Sort {
public static void main(String[] args) {
ArrayList<Integer> list01 = new ArrayList<>();
Collections.addAll(list01,1,3,2);
System.out.println("排序前:" + list01); // 排序前:[1, 3, 2]
// 以匿名内部类的方式传递一个比较器Compartor的实现类参数
Collections.sort(list01, new Comparator<Integer>() {
// 重写比较的规则
@Override
public int compare(Integer o1, Integer o2) {
// return o1 - o2; // 升序
return o2 - o1; // 降序
}
});
System.out.println("排序后:" + list01); // 排序后:[1, 2, 3]
// 创建一个Student集合
ArrayList<Student> list02 = new ArrayList<>();
Collections.addAll(list02,new Student("b泰隆",18),new Student("艾瑞莉娅",19),new Student("卡兹克",
17),new Student("a古拉加斯",18));
System.out.println("排序前:" + list02); // 排序前:[Student{name='泰隆', age=18}, Student{name='艾瑞莉娅', age=19}, Student{name='卡兹克', age=17}]
// 扩展:组合式排序
Collections.sort(list02, new Comparator<Student>() {
// 重写比较规则
@Override
public int compare(Student o1, Student o2) {
// 按照年龄升序
int result = o1.getAge() - o2.getAge();
// 如果两个人年龄相同,再使用姓名的第一个字比较
// 写法一
/*if(result == 0){
result = o1.getName().charAt(0) - o2.getName().charAt(0); // 按照首个字符进行升序排序
}*/
// 写法二,用String类重写的comparaTo方法
if(result == 0){
// result = o1.getName().compareTo(o2.getName()); // 按照首个字符进行升序排序
result = o2.getName().compareTo(o1.getName()); // 按照首歌字符进行降序排序
}
return result;
}
});
System.out.println("排序后:" + list02); // 排序后:[Student{name='艾瑞莉娅', age=19}, Student{name='泰隆', age=18}, Student{name='卡兹克', age=17}]
}
}
2.5 Map接口
2.5.1 Map集合常用6个方法
Map集合的特点:
- Map集合是一个双列集合,一个集合包含两个值(一个key和一个value)
- Map集合中的元素,key和value的数据类型可以相同,也可以不同
- Map集合中的元素,key是不允许重复的,value是允许重复的
- Map集合中的元素,key和value是一一对应的关系
和Collection接口一样,Map接口也规定了6个常用的方法:
public V put(key,value):
把指定的键与指定的值添加到Map集合中。public V get(Object key):
根据指定的键,在Map集合中获取对应的值。boolean containsKey(Object key):
判断集合中是否包含指定的键。public V remove(Object key):
把指定的键 所对应的键值对元素 在Map集合中删除,返回被删除元素的值。public Set<K> keySet():
获取Map集合中所有的键,存储到Set集合中。public Set<Map.Entry(K,V)> entrySet():
获取到Map集合中所有的键值对对象的集合(Set集合)。
前四个方法代码演示如下
public class Demo01Map {
public static void main(String[] args) {
// show01();
// show02();
// show03();
show04();
}
/*
public V put(K key, V value): 把指定的键与指定的值添加到Map集合中。
返回值V:
返回值类型是和传入的value值一样的
存储键值对的时候,如果key不重复,则返回null
存储键值对的时候,如果key重复,会使用新的value替换map中重复的value,返回被替换的value值
注意:
在使用Map接口中的put方法时,一般不接收返回值,采取直接调用的方式
*/
public static void show01(){
// 使用多态创建Map集合
Map<String,String> map = new HashMap<>();
String v1 = map.put("泰隆","艾瑞莉娅");
System.out.println("v1:" + v1); // v1:null
String v2 = map.put("泰隆","迦娜"); // 存储键值对中的key泰隆在原Map集合中已经存在,因此用迦娜将原value覆盖,返回被覆盖的value值
System.out.println("v2:" + v2); // v2:艾瑞莉娅
System.out.println(map); // {泰隆=迦娜}
map.put("泰达米尔","艾希");
map.put("盖伦","卡特琳娜");
System.out.println(map); // {泰隆=迦娜, 盖伦=卡特琳娜, 泰达米尔=艾希}
}
/*
public V remove(Object key): 把指定的键 所对应的键值对元素 在Map集合中删除,返回被删除元素的值。
返回值V:
key存在,则返回被删除的value值
key不存在,则返回空
*/
public static void show02(){
// 创建Map集合对象
Map<String,Integer> map = new HashMap<>();
map.put("泰隆",185);
map.put("艾瑞莉娅",175);
map.put("卡兹克",180);
System.out.println(map); // {卡兹克=180, 艾瑞莉娅=175, 泰隆=185}
Integer v1 = map.remove("卡兹克");
System.out.println(v1); // 180
System.out.println(map); // {艾瑞莉娅=175, 泰隆=185}
// 这里不能使用int接收
Integer v2 = map.remove("肉蛋葱鸡"); // 注意这里不能使用int类型接收,因为删除一个没有的key,会返回null,int类型不能接收null,会报异常
System.out.println(v2); // null
System.out.println(map); // {艾瑞莉娅=175, 泰隆=185}
}
/*
public V get(Object key) 根据指定的键,在Map集合中获取对应的值。
返回值:
key存在,返回对应的value值
key不存在,返回null
*/
public static void show03(){
Map<String,Integer> map = new HashMap<>();
map.put("泰隆",185);
map.put("艾瑞莉娅",175);
map.put("卡兹克",180);
Integer v1 = map.get("泰隆");
System.out.println(v1); // 185
Integer v2 = map.get("肉蛋葱鸡"); // 和remove方法一样,这里会返回null,因此也不能用int接收
System.out.println(v2); // null
}
/*
boolean containsKey(Object key) 判断集合中是否包含指定的键。
包含,返回true
不包含,返回false
*/
public static void show04(){
Map<String,Integer> map = new HashMap<>();
map.put("泰隆",185);
map.put("艾瑞莉娅",175);
map.put("卡兹克",180);
boolean b1 = map.containsKey("泰隆");
System.out.println(b1); // true
boolean b2 = map.containsKey("肉蛋葱鸡");
System.out.println(b2); // false
}
}
后面所提到的两个方法keySet和entrySet都可以用来遍历数组
- 使用keySet遍历数组
遍历步骤:
1.使用Map集合中的方法keySet(),把Map集合中所有的key取出来,存储到一个Set集合中
2.遍历Set集合(迭代器或者增强for),获取集合中的每一个key,要注意使用增强for时的简便用法
3.通过Map集合中的方法get(key),通过key找到value
public class Demo02KeySet {
public static void main(String[] args) {
// 使用多态创建Map对象
Map<String,Integer> map = new HashMap<>();
map.put("泰隆",185);
map.put("艾瑞莉娅",175);
map.put("卡兹克",180);
// 1.使用Map集合中的方法keySet(),把Map集合中所有的key取出来,存储到一个Set集合中
Set<String> set = map.keySet();
// 2.遍历Set集合(迭代器或者增强for),获取集合中的每一个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);
}
// 简化增强for循环遍历
// 使用增强for遍历Set集合
System.out.println("=====================");
for(String key : map.keySet()){
// 3.通过Map集合中的方法get(key),通过key找到value
Integer value = map.get(key);
System.out.println(key + "=" + value);
}
}
}
- 使用entrySet遍历数组
Entry也是一个带有两个泛型参数的接口,只不过Entry借口是Map接口的一个内部接口(相当于是成员内部接口),其作用相当于Python当中字典的item
由于Entry是Map的内部接口,所以通常通过接口名直接方式调用,也就是Map.Entry<K,V>
遍历步骤:
1.使用Map集合中的方法public Set<Map.Entry<K,V>> entrySet(),把Map集合中的所有Entry对象取出来,存储到一个Set集合中
2. 遍历Set集合,获取每一个Entry对象
3.使用Entry对象中的getKey()和getValue()获取键和值
public class Demo01{
public static void main(String[] args){
// 使用多态创建Map集合
HashMap<String String> map = new HashMap();
// 添加元素到集合
map.put("胡歌", "霍建华");
map.put("郭德纲", "于谦");
map.put("薛之谦", "大张伟");
// 获取所有的Entry对象
Set<Map.Entry<String,String>> entrySet = map.entrySet()
// 使用entry对象的getkey和getvalue进行遍历
for(Map.Entry<String,String> entry : entrySet){
String key = entry.getKey();
String value = entry.getValue();
System.out.println(key+"的CP是:"+value);
}
}
}
注意: Map集合和Collection集合不同,前者不能用迭代器或增强for循环直接遍历,只能通过keySet或entrySet进行间接遍历
2.5.2 HashMap集合特点
HashMap基于哈希表的 Map 接口的实现,因此其查询速度相当快。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序(不保证取出的顺序和存储的顺序一致),特别是它不保证该顺序恒久不变。
哈希表的底层实现:
JDK1.8之前:数组 + 哈希表
JDK1.8之后:数组 + 单向链表/红黑树(链表的长度超过8):提高查询的速度
2.5.3 HashMap存储自定义类型键值对
由于Map集合的key不允许重复,因此作为key的自定义类型必须重写equals方法和hashCode方法,以保证编译器能够识别我们所认为的重复元素是什么。但是value值可以重复,所以vlaue的数据类型不需要重写equals和hashCode
// 定义Person类
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 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 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;
}
}
// 定义测试类
public class Demo01HaspMapSavePerson {
public static void main(String[] args) {
// show01();
show02();
}
/*
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集合
for(String key:map.keySet()){
Person value = map.get(key);
System.out.println(value);
}
}
/*
HashMap存储自定义类型键值
key:Person类型
Person类就必须重写hashCode方法和equals方法,以保证key唯一
value:String类型
value可以重复
*/
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("普京",18),"俄罗斯");
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);
}
}
}
2.5.4 LinkedHashMap
集合类型 | 底层实现 | 是否有序 |
---|---|---|
HashSet | 哈希表结构 | 查询速度很快,无序的集合,存储和取出的元素顺序不一致 |
LinkedHashSet | 哈希表+链表结构 | 查询速度很快,有序的集合 |
HashMap | 哈希表结构 | 查询速度很快,无序的集合,存储和取出的元素顺序不一致,是一个线程不安全(多线程)的集合,速度快 |
LinkedHashMap | 哈希表+链表结构 | 查询速度很快,有序的集合 |
Hashtable集合 | 哈希表结构 | 线程安全的(单线程)集合,速度慢,不能存储null值,null键 |
TreeSet | \ | 根据传入的Comparator比较器或者自身继承的Comparable接口中的compareTo方法排序存储 |
ArrayList | byte数组 | 查询速度块,存储速度慢,有序 |
LinkedList | 双向链表 | 查询速度慢,存储速度快,有很多操纵首尾元素的方法 |
LinkedHashMap是HashMap的子类,基于Map接口的哈希表和链表实现,而HashMap只是基于Map的哈希表实现
相比之下LinkedHashMap比HashMao多了一条链接列表,而多的这条链表就是用来记录元素的顺序,因此其具有
可预知的迭代顺序
实际上,对于LinkedHashMap最重要的也就是记住它存储的元素是有顺序的,而HashMap是没有顺序的
用代码证明LinkedHashMap有序,HashMap无序
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); // {a=d, b=b, c=c} key不允许重复,并且无序
Set<Map.Entry<String,String>> entrySet = map.entrySet();
for(Map.Entry<String,String> entry : entrySet ){
System.out.println(entry.getKey() + "--->" + entry.getValue());
}
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); // a=d, c=c, b=b} key不允许重复,并且有序
}
}
2.5.5 Hashtable集合(简单了解)
特点:
- java.util.Hashtable<K,V>集合 implements Map<K,V>接口
- Hashtable:底层是哈希表,是一个线程安全的集合,也就是单线程的集合,速度慢
- 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); // 会将"a"进行覆盖替换
System.out.println(map); // {null=null, b=null}
Hashtable<String,String> table = new Hashtable<>();
// table.put(null,"a"); // NullPointException
// table.put("b",null); // NullPointException
// table.put(null,null); // NullPointException
}
}
2.6 集合特点汇总
下面思维导图转自 :Java集合__知识导图分享