java全端课--集合与泛型

121 阅读12分钟

一、集合(非常重要)

1.1 集合的概念

集合是代表一种容器,一种数据结构。它用来装对象的,也只能用来装对象,不能用来装基本数据类型的值。如果要把基本数据类型的值放到集合中,会自动装箱为包装类的对象。

集合在Java中大体上可以分为两大类:

  • Collection:单列集合,存储一组独立对象的集合。比喻:单身party。
  • Map:双列集合,存储一组键值对(key,value)的集合。比喻:情侣party。

1.2 Collection系列的集合

1.2.1 Collection接口及其API

Collection系列的集合以java.util.Collection接口为根接口,根接口中定义了这个系列集合都支持什么样的操作。操作主线:增、删、改、查、遍历。

Collection接口的实现类非常多,使用频率最高的是ArrayList集合类。以下方法以ArrayList为例演示。

1、增
  • add(一个元素):添加一个元素到当前集合中
  • addAll(另一个集合):添加另一个集合的所有元素到当前集合中
package com.mytest.collection;

import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;

//演示Collection接口的添加方法
public class TestCollection1 {
    @Test
    public void test1(){
        //ArrayList list  = new ArrayList();//正常来说,应该这么写
        Collection list = new ArrayList();//准备了一个容器,装对象的容器,此时没有元素
        /*
        这里这么写有两个用意:
        (1)强调以下 ArrayList 是 Collection接口的实现类,注意一下它们的关系
        (2)ArrayList类的方法比Collection接口要多,因为子类有扩展新的方法。
            现在是学习Collection接口的方法,采用多态引用的话,遵循:编译时看左边,运行时看右边。
            编译时只能看到Collection接口的方法
         */
        list.add("hello");//添加元素到集合容器中
        list.add("world");

        System.out.println(list);//自动调用list的toString方法,说明ArrayList重写了toString方法
        //可以看到元素情况
        //[hello, world]
    }

    @Test
    public void test2(){
        Collection list = new ArrayList();
        list.add("hello");//添加元素到集合容器中
        list.add("world");

        Collection list2 = new ArrayList();
        list2.add("java");//添加元素到集合容器中
        list2.add("hello");

        //想要把第二个集合list2的元素,也添加到第一个集合list中
        list.addAll(list2);//list = list ∪ list2
        //数学中 集合的元素是不能重复的,此时ArrayList这个集合的元素是可以重复的
        System.out.println(list);//[hello, world, java, hello]
    }
}
2、删
  • remove(一个元素对象):删除一个对象
  • removeAll(另一个集合):从当前集合中删除两个集合的交集
  • clear():清空当前集合
  • removeIf(Predicate接口的实现类对象):编写匿名内部类实现Predicate接口,重写public boolean test(Object obj)方法,在方法内部编写根据什么条件删除元素。
package com.mytest.collection;

import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;
import java.util.function.Predicate;

//演示删除操作
public class TestCollection2 {
    @Test
    public void test1(){
        Collection list = new ArrayList();
        list.add("hello");//添加元素到集合容器中
        list.add("world");
        System.out.println("初始:" + list);
        //初始:[hello, world]

        list.remove("world");//删除一个元素
        System.out.println("删除world之后" + list);
        //删除world之后[hello]
    }

    @Test
    public void test2(){
        Collection list = new ArrayList();
        list.add("hello");//添加元素到集合容器中
        list.add("world");

        Collection list2 = new ArrayList();
        list2.add("java");//添加元素到集合容器中
        list2.add("hello");

        System.out.println("初始:" + list);
        //初始:[hello, world]

        list.removeAll(list2);//list = list - (list ∩ list2)
        System.out.println("删除list2集合的元素之后" + list);
        //删除list2集合的元素之后[world]
    }

    @Test
    public void test3(){
        Collection list = new ArrayList();
        list.add("hello");//添加元素到集合容器中
        list.add("world");

        list.clear();
        System.out.println("清空list之后" + list);
        //清空list之后[]
    }

    @Test
    public void test4(){
        Collection list = new ArrayList();
        list.add("hello");//添加元素到集合容器中
        list.add("world");
        list.add("java");
        list.add("world");

        //删除包含a字母的单词
        /*
        removeIf方法是JDK8引入的,
        这个方法的形参是Predicate接口类型
        调用这个方法时,需要传入一个Predicate接口的实现类的对象。
        需要用有名字的类或匿名内部类实现Predicate接口,重写public boolean test(Object obj)方法
        这个方法中,用于编写删除元素的条件。此时方法的形参obj就是代表集合中的元素,
        元素满足某个删除条件,就返回true,否则返回false
         */
        Predicate p = new Predicate() {
            @Override
            public boolean test(Object obj) {
                //判断obj这个元素是不是包含"a"字母
                //编译时形参obj是Object类型,实际上元素是String类型,因为list集合添加了一组字符串
                String str = (String) obj;
                return str.contains("a");//如果包含a字母,contains方法就会返回true,否则返回false
            }
        };
        list.removeIf(p);
        /*
        在集合的removeIf方法的内部,会遍历集合的元素,然后每一个元素都会调用p的test方法进行判断,看它是否满足删除条件。
         */

        System.out.println("删除包含a字母的单词之后:" + list);
        //删除包含a字母的单词之后:[hello, world, world]
    }
}
3、修改(没有)
4、查询
  • boolean contains(一个元素):判断当前集合是不是包含这1个元素
  • boolean containsAll(另一个集合):判断当前集合是不是包含另一个集合的所有元素,即判断另一个集合是不是当前集合的子集。
  • int size():查询当前集合的元素个数
  • boolean isEmpty():判断当前集合是不是空集合
package com.mytest.collection;

import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;

//演示查询的方法
public class TestCollection3 {
    @Test
    public void test1(){
        Collection list = new ArrayList();
        list.add("hello");//添加元素到集合容器中
        list.add("world");

        System.out.println(list.contains("world"));//true
        System.out.println(list.contains("a"));//false
        /*
        这里的contains不是String类的contains方法,而是集合的contains方法
         */
    }

    @Test
    public void test2(){
        Collection list = new ArrayList();
        list.add("hello");//添加元素到集合容器中
        list.add("world");
        list.add("java");
        list.add("world");

        Collection list2 = new ArrayList();
        list2.add("java");//添加元素到集合容器中
        list2.add("hello");
        list2.add("world");

        Collection list3 = new ArrayList();
        list3.add("test");//添加元素到集合容器中
        list3.add("hello");
        list3.add("world");

        System.out.println(list.containsAll(list2));//true
        System.out.println(list.containsAll(list3));//false
        //判断list2或list3是不是list集合的子集
    }

    @Test
    public void test3(){
        Collection list = new ArrayList();
        list.add("hello");//添加元素到集合容器中
        list.add("world");

        //字符串的长度是用length,集合的元素个数用size方法
        System.out.println("元素的个数:" + list.size()); //元素的个数:2
        System.out.println("集合为空吗?" + list.isEmpty()); //集合为空吗?false
    }
}
5、遍历
  • Collection集合不能直接用普通for循环遍历
  • 增强for循环遍历
  • 迭代器遍历(后面单独讲)
package com.mytest.collection;

import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;

public class TestCollection4 {
    @Test
    public void test1(){
        Collection list = new ArrayList();
        list.add("hello");
        list.add("world");

        //普通for循环不能直接用来遍历Collection集合
/*        for(int i=0; i<list.size(); i++){
            System.out.println(list[i]);//错误
        }*/
    }

    @Test
    public void test2(){
        Collection list = new ArrayList();
        list.add("hello");
        list.add("world");

        //增强for循环
        //快捷键是iter
        for (Object obj : list) {//这里obj代表集合的元素
            System.out.println(obj);
        }
    }

    @Test
    public void test3(){
        Collection list = new ArrayList();
        list.add("hello");//添加元素到集合容器中
        list.add("world");

        System.out.println(list);
        //查看集合中的所有元素,以及每一个字符串的长度值
        for (Object obj : list) {
            String str = (String) obj;
            System.out.println(str + "的长度:" +str.length());
        }
    }
}

1.2.2 Collection的子接口Set

Set接口的实现类也很多,所有实现类有一个共同特点:元素不可重复

Set接口的常用实现类:HashSet、LinkedHashSet、TreeSet。

  • HashSet:元素存储没有规律。元素的存储位置与元素的hashCode值有关。
  • LinkedHashSet:元素存储有规律,按照添加的顺序。因为它底层有一个双向链表来记录元素的添加顺序。
    • HashSet和LinkedHashSet要确定元素是否重复,需要调用元素的equals和hashCode方法,如果要根据元素的属性值来确定元素重复情况的话,必须重写这两个方法。
  • TreeSet:元素存储按照大小顺序排列。依赖于Comparable接口 或 Comparator接口。当Comparable的compareTo方法,或Comparator的compare方法结果返回0的话,TreeSet就会认为它们是重复元素。

问:HashSet和LinkedHashSet如何选择?

如果项目业务场景对元素的添加顺序没有要求的话,选择HashSet,因为它更简洁,效率更高。

问:HashSet和TreeSet如何选择?

如果项目业务场景对元素的大小顺序没有要求的话,选择HashSet,因为它更简洁,效率更高。

//想要元素按照长短顺序排列,需要定制比较器
//定义匿名内部类,实现Comparator接口,重写int compare(Object o1, Object o2)方法
//如果长度相同,再看编码值
Comparator c = new Comparator() {
    @Override
    public int compare(Object o1, Object o2) {
        String s1 = (String) o1;
        String s2 = (String) o2;
        int result = s1.length() - s2.length();
        return result != 0 ? result : s1.compareTo(s2);
    }
};

TreeSet set = new TreeSet(c);//一会儿添加元素时,调用c的compare方法来比较元素的大小,决定它的存储位置
//Set接口的方法与Collection接口的一样

set.add("hello");
set.add("world");
set.add("java");
set.add("java");
System.out.println(set);
//[java, hello, world]

二、泛型

2.1 什么是泛型?

泛型:泛化的类型,泛指某种类型。例如:Collection<E><E>就是一种泛化的类型,它只是用于表示元素element的类型,但没有具体说明是哪一种类型(String,Student)等。这种类型需要在使用集合的时候,再来确定,如果没有明确指明,它这里用Object类型处理。

2.2 为什么要用泛型

如果没有泛型的语法,在设计集合类型、比较器接口类型、Predicate接口类型等,这些类型时,就无法“动态”的来确定它们的元素的类型,只能统一按照Object等公共的父类来处理。使用Object类型确实可以接收任意类型的对象,但是这样会导致(1)不安全隐患(2)需要强制类型转换,非常麻烦。

如果有泛型的语法,在设计集合类型、比较器接口类型、Predicate接口类型等,可以用泛化的字母,例如:<E><T>等代表它们未能在设计时确定的元素的类型,当用户使用时,可以“动态”的确定它们的类型,编译器就可以提前做类型的检查。

结论:当我们使用集合、比较器接口类型、Predicate接口类型等这些类型时,需要正确指定泛型。

2.3 泛型类与泛型接口

2.3.1 什么是泛型类与泛型接口

什么是泛型类与泛型接口?

//泛型类
【修饰符】 class 类名<泛型字母>{ 
    
}

 //泛型接口
【修饰符】 interface 接口名<泛型字母>{
    
}

如:

public interface Comparable<T>  :自然比较接口
public interface Comparator<T>  :定制比较接口
public interface Collection<E>  :集合根接口
public interface Set<E>		    :Set集合接口
public interface Predicate<T>	:判断型接口

public class ArrayList<E>
public class HashSet<E>
public class LinkedHashSet<E>
public class TreeSet<E>

2.3.2 如何正确使用泛型类与泛型接口

接口名<类型>
类名<类型>
  • 集合类型 ArrayList<E>HashSet<E>LinkedHashSet<E>TreeSet<E>Collection<E>Set<E>
    • <E>代表的是元素element的类型
  • 比较器:Comparable<T>Comparator<T>
    • <T>代表的是要比较大小的对象的类型 T:Type
  • 判断型接口:Predicate<T>
    • <T>代表的是要进行条件判断的对象的类型
案例1:创建泛型类的对象

应用举例:创建泛型类的对象

ArrayList<E>是一个带泛型的类,HashSet<E>是一个带泛型的类,简称泛型类。
ArrayList<String> list = new ArrayList<>();    
  • 对于集合来说,<>里面应该填写集合元素的具体类型
  • 从JDK7开始,右边<>中的类型可以省略,它可以根据左边<>中的类型自动推断,但是<>得保留
  • 如果<>省略的话,会有警告,因为它认为你擦除了泛型

2.3.3 如何自定义泛型类或泛型接口(了解)

需求:

  • 定义一个学生类XueSheng,它的姓名是String,它的成绩是未能确定的类型。
  • 语文老师说,成绩应该是字符串类型,用优秀、良好、及格等表示。
  • 数学老师说,成绩应该是86.5的小数值
  • 英语老师说,成绩应该是A,B,C,D等

现在要求这个学生类需要同时能满足这些老师的要求。

【修饰符】 class 类名<泛型字母>{
    
}
  • 定义泛型类时,这里<>里面不推荐使用单词,建议使用单个大写字母
  • 使用泛型类时,如果要明确<泛型字母>具体代表什么类型时,需要指定引用数据类型,不能指定基本数据类型。如果是基本数据类型,得改用它的包装类。
  • 在类名或接口名后面声明的<泛型字母>不能用于静态成员。每一个对象或每一次实现这个接口都要单独指定类型。

2.4.4 答疑

、泛型字母可以多个吗
【修饰符】 class 类名<泛型字母1,泛型字母2,泛型字母3>{
    
}

public class XueSheng<S,T,A>{ //多个泛型字母
    
}
XueSheng<String,Integer,String> x = new XueSheng<>(实参列表);//就需要指定多个泛型字母的具体类型

2.4 泛型方法

2.4.1 什么是泛型方法

【修饰符】 class 类名{
     //在方法的返回值类型前面定义了新的<泛型字母>,这样的方法称为泛型方法
    【修饰符】 <泛型字母> 返回值类型 方法名(【形参列表】){
        
    }
}

2.4.2 使用泛型方法

泛型方法的<T>通常用于方法的形参类型,此时<T>类型通常由实参的类型自动确定。

2.5 通配符?

2.5.1 通配符的应用场景

当我们使用一个泛型类或泛型接口时,仍然无法确定它的<泛型字母>该用什么具体类型时,可以用<?>通配符来指定。

2.5.2 使用形式

1、<?>

所有类型通通都匹配。

2、<? extends 上限>
3、<? super 下限>

2.5.3 缺点

一旦泛型类<?> 或 泛型类<? extends 上限> 或 泛型类<? super 下限>,这些泛型类基本上只能看,不能修改元素或泛型类型对应的属性。

它们3个的应用场景通常出现在方法的形参列表中。

2.6 和 <?>的对比

  • <T>T可以当做独立的类型使用,?不能独立使用,只能结合泛型类或泛型接口使用。
  • 同一个方法中,<T>的类型一定是相同的,但是<?>类型可以是不同的。
  • <T>在声明时,只能指定上限,上限可以是多个,多个上限用&连接,多个上限中类只能有1个,接口可以多个。<?>可以指定上限,也可以指定下限。