Java学习笔记:泛型、集合与数组

本文介绍了Java中的泛型,强调了泛型程序设计的作用,包括类型擦除、通配符的使用以及泛型的限制。接着讲解了Java集合框架,探讨了不同集合实现的效率,如ArrayList、LinkedList、HashSet、TreeSet、Queue和Deque等,并提到了映射HashMap和TreeMap的特性。文章还涉及了集合视图和线程安全的概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


泛型


写在前头的重点总结

  • 泛型程序设计的作用就是让编写的代码可以被不同类型的对象所重用
  • 泛型的使用,即在声明类或定义一个泛型方法时加形如<T>的菱形,里面是占位符,这个占位符就指代泛型所包含的所有类型
  • 泛型类或方法中的占位符类型如果要加以约束: <T extends Comparable & Serializable>, 而不是用 implements
  • 类型擦除:类型擦除之所以产生,是因为对于虚拟机来讲不存在泛型类型对象,而只有普通的类和方法。虚拟机处理时要把泛型类型处理为对应的原始类型,这个过程就叫做类型擦除。
  • 为了保持多态特性,类型擦除可能会产生一些错误,因此使用Java泛型时要考虑一些限制:
    • 不能用基本类型实例化类型参数,如 不能用double而要用Double,因为擦除后的Object域不能存储double值
    • 运行时类型查询只适用于原始类型
    • 不能创建参数化类型的数组,如Pair<String>[] table = new Pair<String>[10]; 唯一的安全且有效的方法是使用ArrayList:ArrayList<Pair<String>>
    • 不能实例化类型变量,即不能使用new T(...), new T[...] 或T.class这样的表达式中的类型变量
    • 不能构造泛型数组
    • 禁止使用带有类型 变量的静态域或静态方法
    • 不能抛出或捕获泛型类的实例
  • 通配符—— ?:另一种为泛型变量类型施加约束的方法,如public static void printBuddies( Pair<? extends Employee>),这个方法, 则可以将Pair<Manager> (Manager继承Employee)作为参数传入该方法。
  • 如果反射的对象是泛型类的实例,那么通过反射方法并不能得到太多关于泛型类型参数的信息。

详细内容

https://www.cnblogs.com/dyj-blog/p/8990050.html
 


集合


  • Java集合类库将接口与实现分离,如队列Queue接口的实现方式有两种:如果需要一个循环数组队列,就可以使用ArrayDeque类;如果需要使用一个链表队列,就直接使用LinkedList类,该类实现了Queue接口。在构建对应的集合对象时,只有使用具体的类才有意义,否则就不知道究竟使用了哪种具体实现。
    Queue<Customer> expressLane = new ArrayDeque<>(100);
    expressLane.add(new Customer("Harry"));
    
    //利用这种方式,一旦改变了想法,可以轻松使用另外一种不同的实现,即修改调用构造器之处
    Queue<Customer> expressLane = new LinkedList<>();
    expressLane.add(new Customer("Henry"));
    
    //本文下面涉及到的两行代码
    List<String> staff = new LinkedList<>();

    不同实现方式的效率也是不同的,比如循环数组队列比链表更高效,但是它是一个有界集合,即容量有限。如果程序中要收集的对象数量没有上限,就最好使用链表来实现。

  • Java类库中集合类的基本接口是Collection接口,该接口有一些基本方法,例如: boolean add(E element);以及 Iterator<E> iterator(); 其中iterator方法用于返回一个实现了Iterator接口的对象,即迭代器,用来访问集合中的每一个 元素。
  • 应该将Java迭代器认为是位于两个元素之间,当调用next()时,迭代器就越过下一个元素,并返回刚刚越过的元素的 引用。一个有用的推论是,可以将Iterator.next()与InputStream.read()看作等效的。 另外,Iterator的next()方法和remove()方法的调用具有依赖性,调用remove()方法之前必须调用next()方法,这样就会删掉刚刚next()方法所越过的元素。对于LinkedList来说,它实现了一个Iterator接口的子接口ListIterator接口,该接口多一个与next()方法相对的previous()方法,可以用来反向遍历与删除元素。
    import java.util.Iterator;
    import java.util.LinkedList;
    import java.util.List;
    import java.util.ListIterator;
    
    public class JavaSetsTest {
        public static void main(String[] args) {
            linkedListTest();
    
        }
    
        /**
         * LinkedList数据结构为链表,从链表中间删除一个元素,即需要被删除元素附近的链接。
         * 链表与泛型集合之间有一个非常重要的区别,那就是链表是一个有序集合,每个对象的位置十分重要。
         * LinkedList的add方法将对象添加到链表的尾部。但是如果需要将对象添加到链表的中间,那么这种
         * 依赖于位置的add方法将由迭代器负责。只有对自然有序的集合使用迭代器添加元素才有实际意义,
         * 若其中的元素全都无序,如set类型,因此,Iterator接口中没有add方法,而是集合类库提供一个
         * 子接口ListIterator中包含add方法。另外ListIterator接口的previous()和hasPrevious()方法还
         * 可以用来反向遍历链表。
         */
        public static void linkedListTest() {
            List<String> staff = new LinkedList<>();
            staff.add("tomy");
            staff.add("bob");
            staff.add("Alibi");
            /*Iterator<Sting>如果改为Iterator,那么staff中的默认元素为对象,则后两句
            String first\second = iterator.next()后面还需要添加toString()方法将对象类型转为字符串类型。*/
            Iterator<String> iterator = staff.iterator();  //获取该集合的迭代器.
            String first = iterator.next();
            String second = iterator.next();
            System.out.println(first);
            System.out.println(second);
            iterator.remove();   // remove方法将在迭代器位置之前的元素删掉
            //iterator.add(); //add将会报错,因为Iterator接口中没有add方法。
            ListIterator<String> listIterator = staff.listIterator();
            System.out.println(listIterator.next());
            listIterator.add("Ash");       // ListIterator迭代器的add方法将在迭代器位置之前添加一个新对象
            System.out.println(listIterator.next());
            listIterator.add("Thermite");
            //previous()方法获取迭代器位置之前的对象。执行完该方法后,迭代器位置向前移动一个单位。
            System.out.println(listIterator.previous());
            // set()方法用一个新元素来取代调用next或previous方法返回的上一个元素。
            listIterator.set("New Value");
            /*当一个迭代器在修改集合时,另一个迭代器对其进行遍历,一定会出现混乱的情况,例如一个
            迭代器指向另一个迭代器刚刚删除的元素前面,现在这个迭代器就是无效的,并且不该再使用。
            链表迭代器的设计使其能够检测到这种修改。如果迭代器发现它的集合被另一个迭代器修改了,
            或者被该集合的自身方法修改了,就会抛出一个ConcurrentModificationException异常。
            为了避免这种情况,可以根据需要给容器附加许多的迭代器,但是这些迭代器只能读取列表,
            另外单独附件一个能够读和写的迭代器。*/
        }
    }

     

具体的集合


(以上两张图转载于https://blog.youkuaiyun.com/Adair81/article/details/79515074

 

部分常用具体集合的详细介绍(集合test代码在本节下方):

  1. 链表LinkedList:可以很轻松地在中间删除一个元素。链表是一个有序集合,add方法将对象添加到链表的尾部。如果想要添加到链表的中间,那么就要依靠迭代器ListIterator(而不是Iterator,因为Iterator没有add方法,原因是Iterator是面向所有集合的,其中一些集合不是有序的)。该add方法是在迭代器所处的位置之前而不是之后进行添加元素的。
  2. 数组列表ArrayList与数组:由于数组在初始化的时候就已经确定好其长度了,当需要一个长度不确定的数组时就可以使用数组列表。相比于链表,数组列表可以通过索引值来取出或删除元素——数组与数组列表 - 陆放为
  3. 散列集HashSet:是一种无序的、元素不重复的集合,当不在意元素的顺序时,可以快速查找元素。散列集的实现方式是——每个对象都会通过其实例域生成一个唯一的散列码,通过该散列码作为该对象的标识或查找时使用的索引。
  4. 树集TreeSet:与散列集十分类似,元素不重复,可以较快地对其中的元素进行添加、查找(但速度低于散列集),但树集是一个有序集合。树集将会按照顺序排列元素,因此遍历时的元素顺序与添加元素的次序是无关的,其排序使用红黑树结构完成。由于其有序性,则树集中的元素必须实现Comparable接口,或者构造集时必须提供一个实现了Comparator接口的比较器类实例。
  5. 队列与双端队列:队列(Queue接口)可以有效地在尾部添加一个元素,在头部删除一个元素。有两个端头的队列即双端队列(Deque),可以有效地在头部和尾部同时添加或删除元素。ArrayDeque和LinkedList类都提供双端队列,而且在必要时可以增加队列的长度。
  6. 优先级队列PriorityQueue:优先级队列中的元素可以按照任意的顺序插入,但总是按照排序的顺序进行检索,也就是说无论何时调用remove()方法,总会获得当前优先级队列中排序顺序最小的元素,该集合的典型例子是任务调度,不同优先级的任务以随机顺序添加到队列,每当启动新任务时,都将优先级最高的任务从队列中删除(习惯上将1设置为最高优先级)。同样地,PriorityQueue中的元素也需要实现Comparable接口或提供一个Comparator类实例。
  7. 映射 HashMap、TreeMap:用来存放键/值对,如果提供了键,就能查找到值。HashMap对进行散列,TreeMap用的整体顺序对元素进行排序,并将其组织成搜索树。因此如果不需要按照排列顺序访问键的话,最好选择散列映射HashMap。可以通过Map.keySet() values() entrySet()等方法获取该映射的视图(view)。

JavaSetsTest.java

import java.time.LocalDate;
import java.util.*;

public class JavaSetsTest {
    public static void main(String[] args) {
        //linkedListTest();
        //setTest();
        //treeSetTest();
        //priorityQueueTest();
        hashMapTest();
    }

    /**
     * LinkedList数据结构为链表,从链表中间删除一个元素,即需要被删除元素附近的链接。
     * 链表与泛型集合之间有一个非常重要的区别,那就是链表是一个有序集合,每个对象的位置十分重要。
     * LinkedList的add方法将对象添加到链表的尾部。但是如果需要将对象添加到链表的中间,那么这种
     * 依赖于位置的add方法将由迭代器负责。只有对自然有序的集合使用迭代器添加元素才有实际意义,
     * 若其中的元素全都无序,如set类型,因此,Iterator接口中没有add方法,而是集合类库提供一个
     * 子接口ListIterator中包含add方法。另外ListIterator接口的previous()和hasPrevious()方法还
     * 可以用来反向遍历链表。
     */
    private static void linkedListTest() {
        List<String> staff = new LinkedList<>();
        staff.add("tomy");
        staff.add("bob");
        staff.add("Alibi");
        /*Iterator<Sting>如果改为Iterator,那么staff中的默认元素为对象,则后两句
        String first\second = iterator.next()后面还需要添加toString()方法将对象类型转为字符串类型。*/
        Iterator<String> iterator = staff.iterator();  //获取该集合的迭代器.
        String first = iterator.next();
        String second = iterator.next();
        System.out.println(first);
        System.out.println(second);
        iterator.remove();   // remove方法将在迭代器位置之前的元素删掉
        //iterator.add(); //add将会报错,因为Iterator接口中没有add方法。
        ListIterator<String> listIterator = staff.listIterator();
        System.out.println(listIterator.next());
        listIterator.add("Ash");       // ListIterator迭代器的add方法将在迭代器位置之前添加一个新对象
        System.out.println(listIterator.next());
        listIterator.add("Thermite");
        //previous()方法获取迭代器位置之前的对象。执行完该方法后,迭代器位置向前移动一个单位。
        System.out.println(listIterator.previous());
        // set()方法用一个新元素来取代调用next或previous方法返回的上一个元素。
        listIterator.set("New Value");
        /*当一个迭代器在修改集合时,另一个迭代器对其进行遍历,一定会出现混乱的情况,例如一个
        迭代器指向另一个迭代器刚刚删除的元素前面,现在这个迭代器就是无效的,并且不该再使用。
        链表迭代器的设计使其能够检测到这种修改。如果迭代器发现它的集合被另一个迭代器修改了,
        或者被该集合的自身方法修改了,就会抛出一个ConcurrentModificationException异常。
        为了避免这种情况,可以根据需要给容器附加许多的迭代器,但是这些迭代器只能读取列表,
        另外单独附件一个能够读和写的迭代器。*/
    }

    /**
     * 散列集
     */
    private static void setTest() {
        Set<String> words = new HashSet<>(); //HashSet是Set接口的实现类
        long totalTime = 0;
        try (Scanner in = new Scanner(System.in)) {
            while (in.hasNext()) {
                String word = in.next();
                long callTime = System.currentTimeMillis();
                words.add(word);
                callTime = System.currentTimeMillis() - callTime;
                totalTime += callTime;
            }
        }
        Iterator<String> iter = words.iterator();
        for (int i = 1; i <= 20 && iter.hasNext(); i++) {
            System.out.println(iter.next());
            System.out.println(" . . .");
            System.out.println(words.size() + " distinct words." + totalTime + " milliseconds.");
        }
    }

    /**
     * TreeSet实现了SortedSet接口,而SortedSet接口继承自Set
     * 要使用树集,则其中的元素必须实现Comparable接口,或者构造集时必须提供一个实现了Comparator接口的类的实例
     * 树集将会按照顺序排列元素,因此遍历时的顺序与添加元素的顺序是无关的,其排序使用红黑树结构完成
     */
    private static void treeSetTest() {
        SortedSet<String> sort = new TreeSet<>();
        sort.add("Bob");
        sort.add("Amy");
        sort.add("Thermite");
        for (String s :sort) System.out.println(s);
    }

    /**
     * PriorityQueue使用的数据结构成为堆heap,堆是一个可以自我调整的二叉树,对树执行添加(add)和删除(remove)
     * 操作,可以让最小的元素移动到根,而不必花费时间对元素进行排序。
     * PriorityQueue中的元素,进行迭代时不一定会按照顺序,但是remove方法一定是按照顺序的。
     */
    private static void priorityQueueTest() {
        PriorityQueue<LocalDate> pq = new PriorityQueue<>();
        pq.add(LocalDate.of(1906, 12, 9));
        pq.add(LocalDate.of(1815, 12, 10));
        pq.add(LocalDate.of(1903, 12, 3));
        pq.add(LocalDate.of(1910, 6, 22));

        System.out.println("迭代元素...");
        for (LocalDate date : pq) {
            System.out.println(date);
        }
        System.out.println("删除元素...");
        while (!pq.isEmpty()) {
            System.out.println(pq.remove());
        }
    }

    /**
     * 每当往映射中添加对象时,必须同时提供一个键。在下面的方法中,键是一个字符串,对应的值是LocalDate对象
     * 更新映射项的时候要注意,get方法中的传入参数Key对应的值如果不存在的话,是会返回null的,
     * 使用getOrDefault方法可以在对应映射不存在的情况下返回一个默认值
     *
     * 集合框架不认为映射是一种集合,但是可以通过Map.keySet() values() entrySet()方法获取该映射的视图(view)
     */
    private static void hashMapTest() {
        Map<String, LocalDate> births = new HashMap<>();
        births.put("JayChow", LocalDate.of(1980, 12, 6));
        births.put("Jolin", LocalDate.of(1979, 6, 20));
        births.put("ZhangXinYu", LocalDate.of(1997, 8, 8));
        births.put("GuoJinYi", LocalDate.of(1997, 5, 1));

        //打印所有键值对的信息,该顺序与put的顺序并不同
        System.out.println(births);
        //使用forEach()方法迭代所有键值对,该顺序与直接打印births的顺序是相同的
        births.forEach((k,v ) ->
                System.out.println("Key = "+ k +" , Value = " + v));
        //删除一个条目
        births.remove("JayChow");
        //替换一个条目
        births.replace("Jolin", LocalDate.of(1988, 8, 8));
        System.out.println(births);
        //查找一个值, 如果找不到则返回null
        System.out.println(births.get("ZhangXinYu"));
        System.out.println(births.get("Nobody"));

        //通过Map.keySet() values() entrySet()方法获取该映射的视图(view)
        Set<String> keys = births.keySet();
        System.out.println(keys);
        Collection<LocalDate> values = births.values();
        System.out.println(values);
        
        //以下方法可以简单地用Map.forEach()方法代替
        Set<Map.Entry<String, LocalDate>> entrySet = births.entrySet();
        for (Map.Entry<String, LocalDate> entry : entrySet) {
            String k = entry.getKey();
            LocalDate v = entry.getValue();
            System.out.println("k:" + k + ", v:" + v);
        }
    }
}

视图与包装器:上面的代码最后部分使用Map.keySet() values() entrySet()方法获取了映射births的视图(view)。比如keySet()方法返回一个实现Set接口的类的对象,而这个类的方法可以对原映射进行操作(并不是单向关系,而是双向关联!),这种集合就成为视图。

视图的作用很广泛,例如Java集合类库的设计者使用视图机制来确保常规集合的线程安全,而不是实现线程安全的集合类,如Collections类的静态synchronizedMap方法可以将任何一个映射表转换成具有同步访问方法的Map:

Map<String, LocalDate> map = Collcetions.synchronizedMap(new HashMap<String, LocalDate>());

这样就可以多线程访问该map对象了,get和put方法将会都是同步操作的。

视图的具体内容参考:Java集合——视图与包装器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值