Java学习笔记 12、集合框架(二)

简介: Java学习笔记 12、集合框架(二)

LinkedHashSet



LinkedHashSet:是HashSet的子类,无相关自己实现的类。其中实际上使用的是LinkedHashMap(同样是HashMap子类),能够顺序访问插入进来的数。


底层使用的是数组+双向链表形式,其中访问数据都是有序的。如下图清晰看出为什么能够顺序访问存储的数据:



要求:向Set中添加的数据,其所在类一定要重写hashCode()与equals()方法。极可能保持两个方法一致性。


说明:LinkedHashSet插入性能略低于HashSet,但在迭代访问Set里的全部元素有很好的性能。



TreeSet



TreeSet:是 SortedSet 接口的实现类,可以确保元素处于排序状态,底层使用红黑树结构存储数据。


两种排序方式:自然排序与定制排序。默认情况下采用自然排序(map根据key自然排序)。

特点:有序,查询速度比List快。


两种排序实现


默认是使用的自然排序(很多类中本身就实现了Comparable,add()添加中调用public int compareTo(T o);即可进行比对);另一种则是定制排序,需要实现Comparable接口传入到TreeSet构造器中,见如下:


自然排序:


import java.util.*;
public class Main {
    public static void main(String[] args) {
        TreeSet<Person> set = new TreeSet<>();
        set.add(new Person("changlu",12));
        set.add(new Person("liner",10));
        set.add(new Person("changlu",8));
        //遍历
        Iterator<Person> iterator = set.iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
}
class Person implements Comparable{
    private String name;
    private Integer age;
    public Person(String name, Integer 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;
        if (name != null ? !name.equals(person.name) : person.name != null) return false;
        return age != null ? age.equals(person.age) : person.age == null;
    }
    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + (age != null ? age.hashCode() : 0);
        return result;
    }
    @Override
    public int compareTo(Object o) {
        if(o instanceof Person){
            Person p = (Person) o;
            //先按照String排序
            int result = this.name.compareTo(p.name);
            if(result == 0){//0表示相等
                return this.age.compareTo(p.age);
            }
            return result;
        }else{
            throw new RuntimeException("类型不匹配");
        }
    }
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}



自然排序就是会根据添加类中通过实现Comparable接口实现的compareTo方法来进行比对添加。对于一些JDK中提供的类已经实现了Compareable接口,而自定义的则需要手动实现该接口并实现其方法。

重写equals()、hashCode()法是为了让Set集合中不出现重复的元素,compareTo方法是去实现排序的效果。



自定义排序:


import java.util.*;
public class Main {
    public static void main(String[] args) {
        //实现一个匿名接口类,实现其中的方法,让字符串倒序排列
        Comparator<String> comparator = new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return -o1.compareTo(o2);
            }
        };
        TreeSet<String> set = new TreeSet<String>(comparator);
        set.add("changlu");
        set.add("liner");
        //遍历
        Iterator<String> iterator = set.iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
}



通过实现Comparator的匿名接口类将其放入到TreeSet的构造器中,之后执行add()方法则里排序会根据Comparator中的compareTo方法进行比对。




源码分析


下面只是我自己简单的分析:


//TreeSet源码
public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable{
    private transient NavigableMap<E,Object> m;
    public TreeSet() {
        //实际上创建了TreeMap的实例
        this(new TreeMap<E,Object>());
    }
    //实际上调用了TreeMap的有参构造传入了comparator(即定制排序)
    public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
    }
    //执行添加操作实际上就是调TreeMap的put()方法
    public boolean add(E e) {
        return m.put(e, PRESENT)==null;
    }
    ...
}
//TreeMap源码
public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable{
    //内部comparator实例
    private final Comparator<? super K> comparator;
    //该构造器将外部自定义comparator引用传给内部属性
    public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }
    //put()方法:其中就会根据情况使用自然排序或者定制排序
    public V put(K key, V value) {
        ...
        Comparator<? super K> cpr = comparator;//接收本类中的comparator(初始为null)
        if (cpr != null) {//这里就是判断是使用自然排序还是定制排序
            //不为null表示,说明我们之前传入了Comparator实例,使用定制排序
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);//执行实现了compare()方法
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            //使用自然排序,一般就是直接调集合元素中的compareTo()方法
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);//这里进行了调用
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }    
        ...
    }
}



粗略的看了下源码,这里只针对使用自然排序还是定制排序进行分析


Set相关面试题

(1)、在list中取出重复数字值,要求尽量简单


import java.util.*;
public class Main {
    public static List<Integer> duplicateList(List<Integer> list){
        HashSet<Integer> set = new HashSet<>(list);//或者使用set.addAll(list)添加
        return new ArrayList<>(set);
    }
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(12);
        list.add(15);
        list.add(12);
        list.add(26);
        list.add(15);
        list = duplicateList(list);
        //遍历
        Iterator<Integer> iterator = list.iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
}



(2)、hashset增加或删除值的判断


class Person{
    public String name;
    public 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;
        if (age != person.age) return false;
        return name != null ? name.equals(person.name) : person.name == null;
    }
    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
public class exerTest {
    @Test
    public void test(){
        HashSet hashSet = new HashSet();
        Person p = new Person("Tom",100);
        Person p1 = new Person("Tom1",101);
        hashSet.add(p);
        hashSet.add(p1);
        System.out.println(hashSet);//[Person{name='Tom', age=100}, Person{name='Tom1', age=101}]
        p.age = 102;
        hashSet.remove(p);
        System.out.println(hashSet);//[Person{name='Tom', age=102}, Person{name='Tom1', age=101}]
        hashSet.add(new Person("Tom",102));
        System.out.println(hashSet);
        //[Person{name='Tom', age=102}, Person{name='Tom', age=102}, Person{name='Tom1', age=101}]
        hashSet.add(new Person("Tom",100));
        System.out.println(hashSet);
        //[Person{name='Tom', age=102}, Person{name='Tom', age=102}, Person{name='Tom1', age=101}, Person{name='Tom', age=100}]
    }
}


50行修改原先p的age值,接着执行remove()方法删除无效,过程:首先比对hashcode值,找到了确定位置,接着使用equals()方法比较值,没有相同的,所以删除失败。

第54行添加了一个对象,这里添加成功,过程:首先会推出hashcode值,没有与之相同的则会直接添加。注意这里添加的虽然和之前修改的一个对象属性相同,但是hashcode值不同,所以添加成功。

第58行我们又添加了一个对象,这个对象中的属性与原来没有修改的p相同,所以hashcode值与p是同一个位置,接着使用equals()方法比对,发现没有值与其相同,最后添加成功。


hashCode()方法重写介绍
class Person{
    String name;
    Integer age;
    //这是通过IDEA生成的hashCode()方法
    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + (age != null ? age.hashCode() : 0);
        return result;
    }
}


result是本地方法得到的一个hash值,选择一个系数值31与result相乘。

选择系数的时候要选择尽量大的稀疏,如果计算出来的hash地址越大,所谓的冲突也就越少,查找起来效率也会提高(减少冲突)。

这里的31也可以使用i*31=(i<<5)-1,乘以2*5-1=31,很多虚拟机中都有相关优化。(提高算法效率)

31是一个素数,数组作用就是如果我用一个数字来乘以这个素数,最终出来的结果只能被本身和被乘数还有1来整除!(减少冲突)


Map接口及常用实现类


Map接口

Map接口:就是一个单独的接口interface Map<K,V> ,用于保存具有映射关系的数据:key-value,key与value都可以是任何引用类型的数据。


key使用Set来存放,不允许重复,即同一个Map对象所对应的类必须重写equals()、hashCode()方法。一般使用String类作为key。

通过指定的key总能找到唯一、确认的value。



key使用的是Set(无序、不可重复),Values使用的Collection存储(无序、可重复),每个键值对则为Entry(无序、不可重复)。

常用实现类:


HashMap:作为Map的主要实现类:线程不安全,效率高;可以存储null的key与value。

LinkedHashMap:保证在遍历map元素时,可以按照添加的顺序实现遍历。

原因:在原本的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。对于频繁的遍历操作,执行效率比HashMap高。

TreeMap:按照添加的key-value进行排序,实现排序遍历。会考虑key的自然排序或定制排序,底层使用了红黑树。

HashTable:线程安全,效率低,不能存储为null的key和value(若是value为空直接手动抛出异常,而key报异常是因为调用了key的hashcode()方法若为null则是空指针)。

Properties:常用来处理配置文件,key和value都是String类型。

针对于HashMap、LinkedHashMap想要构造一个线程安全的则可用Collections.synchronizedMap(new HashMap(...))


针对于TreeMap想要构造线程安全的可用Collections.synchronizedSortedMap(new TreeMap(...)) 。


常用方法:




HashMap



HashMap:是利用哈希表原理来存储元素的集合,有两个参数影响性能:初始容量和加载因子。


不同版本变化:


JDK7:底层是数组+链表(链地址法)。


JDK8:数组+链表(或红黑树)






不同版本底层实现原理


JDK7的实现原理如下:



简述:初始创建长度为16的数组,执行put()后,首先获取哈希值来得到存放位置,判断当前位置是否为空,为空插入成功;不为空来去比对其中的哈希值是否相同,若都不相同则添加成功;若相同,再比较equals()方法若是返回false添加成功,返回true为失败。

扩容情况:扩容2倍。

JDK8的实现原理如下:



同样也是扩容2倍。

转换不同数据结构:当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后, 下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。


若是我们已知HashMap中的元素个数,建议预设元素个数能够有效提升HashMap的性能。



重要常量


DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16。

MAXIMUM_CAPACITY : HashMap的最大支持容量,2^30 。

DEFAULT_LOAD_FACTOR:HashMap的默认加载因子 。

TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树 。

UNTREEIFY_THRESHOLD:Bucket中红黑树存储的Node小于该默认值,转化为链表 。

MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量。(当桶中Node的 数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,此时应执行 resize扩容操作这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4 倍。)

table:存储元素的数组,总是2的n次幂 。

entrySet:存储具体元素的集 。

size:HashMap中存储的键值对的数量 。

modCount:HashMap扩容和结构改变的次数。

threshold:扩容的临界值,=容量*填充因子 loadFactor:填充因子。


LinkedHashMap



LinkedHashMap:是HashMap的子类,在HashMap存储结构基础上,使用了双向链表来记录添加元素的顺序。与LinkedHashSet类似,LinkedHashMap可以维护Map的迭代顺序(与插入顺序一致)。


看一下HashMap中的内部类及LinkedHashMap中的内部类:


//HashMap中的内部类Node
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
    ...
}
//LinkedHashMap的内部类Entry
static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;//包含一个前后链表
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}


与HashMap不同之处就是维护了这个Entry双向链表,通过该Entry来保存前后顺序


TreeMap



TreeMap:底层是红黑树,存储 Key-Value 对时,需要根据 key-value 对进行排序。保证所有的 Key-Value 对处于有序状态。对于TreeMap可以使用自然排序与定制排序(可看前面TreeSet),注意是对key进行排序。


TreeMap的containsKey、get、put、remove方法提供了log(n)的时间开销。


HashTable



HashTable:线程安全(方法都上了锁),实现了一个哈希表,能够将键映射到值,key与value都不能为空。


源码分析:看一下为什么key与value不为为空


public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {//一旦value为空则抛出异常
        throw new NullPointerException();
    }
    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();//若为key为空,这里调用hashCode()则会出现访问空指针异常
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }
    addEntry(hash, key, value, index);
    return null;
}



Properties



Properties:是HashTable的子类,该对象用于处理属性文件,其中的key、value值都是String类型的。


常用于读取properties配置文件,读取方法一般使用:setProperty(String key,String value)方法和 getProperty(String key)


实际上这个类给我们封装了文件读取流的操作,可从配置文件中读取key、value键值对存储到Map集合中,接着我们使用方法来获取键值对,一般使用于读取数据库配置文件:


实例演示


public static void main(String[] args) {
    Properties properties = new Properties();
    try {
        properties.load(new FileInputStream("jdbc.properties"));
        String username = properties.getProperty("username");
        String password = properties.getProperty("password");
        System.out.println(username+":"+password);
    } catch (IOException e) {
        e.printStackTrace();
    }
}




一行写一对key、value键值对,查看源码是每次读取一行


Map遍历方法


public static void main(String[] args) {
    Map<String,Integer> map = new HashMap<>();
    map.put("changlu",18);
    map.put("liner",21);
    //遍历key
    Set<String> set = map.keySet();
    for (String s : set) {
        System.out.println(s);
    }
    //遍历value
    Collection<Integer> values = map.values();
    for (Integer value : values) {
        System.out.println(value);
    }
    //遍历Entry
    Set<Map.Entry<String, Integer>> entries = map.entrySet();
    for (Map.Entry<String, Integer> entry : entries) {
        System.out.println(entry.getKey()+":"+entry.getValue());
    }
}



keySet():获取由所以的key组成的set集合

values():获取由所有的value组成的collection集合。

entrySet():获取包含key、value的set集合。


相关面试题

例1:负载因子值的大小,对HashMap有什么影响


负载因子的大小决定了HashMap的数据密度。

负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长, 造成查询或插入时的比较次数增多,性能会下降。

负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的 几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性 能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建 议初始化预设大一点的空间。

按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7~0.75,此时平均检索长度接近于常数。


四、Collections工具类



单独一个分支,是一个包装类。

Collections:是一个操作Set、List、Map等集合的工具类,提供了一系列静态方法来对集合元素进行排序、查询、修改等操作,提供了对集合对象设置不可变,对集合对象实现同步控制等方法(上锁)。


Arrays:是一个操作数组的工具类

相关方法


排序方法:


reverse(List):反转 List 中元素的顺序 。

shuffle(List):对 List 集合元素进行随机排序 。

sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序 。

sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序 。

swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换。

查找,替换方法:


Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素 。

Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回 给定集合中的最大元素。

Object min(Collection) :根据元素的自然顺序,返回给定集合中的最小元素 。

Object min(Collection,Comparator) :根据 Comparator 指定的顺序,返回 给定集合中的最小元素 。

int frequency(Collection,Object):返回指定集合中指定元素的出现次数。

void copy(List dest,List src):将src中的内容复制到dest中 。

boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值。

同步控制方法:将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。


public static Collection synchronizedCollection(Collection c);

public static Set synchronizedSet(Set s);

public static List synchronizedList(List list);

public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m);

public static SortedSet synchronizedSortedSet(SortedSet s);:例如TreeSet。

public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m);:例如TreeMap。


参考资料

[1]. fail-fast


[2]. 真正搞懂hashCode和hash算法(面试官都馋哭了)


[3]. 面试常问:什么是红黑树?


[4]. 尚硅谷_Java零基础教程-java入门必备-适合初学者的全套完整版教程(宋红康主讲)

相关文章
|
10天前
|
人工智能 前端开发 Java
基于开源框架Spring AI Alibaba快速构建Java应用
本文旨在帮助开发者快速掌握并应用 Spring AI Alibaba,提升基于 Java 的大模型应用开发效率和安全性。
基于开源框架Spring AI Alibaba快速构建Java应用
|
10天前
|
消息中间件 Java 数据库连接
Java 反射最全详解 ,框架设计必掌握!
本文详细解析Java反射机制,包括反射的概念、用途、实现原理及应用场景。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
Java 反射最全详解 ,框架设计必掌握!
|
1天前
|
JSON Java Apache
非常实用的Http应用框架,杜绝Java Http 接口对接繁琐编程
UniHttp 是一个声明式的 HTTP 接口对接框架,帮助开发者快速对接第三方 HTTP 接口。通过 @HttpApi 注解定义接口,使用 @GetHttpInterface 和 @PostHttpInterface 等注解配置请求方法和参数。支持自定义代理逻辑、全局请求参数、错误处理和连接池配置,提高代码的内聚性和可读性。
|
1天前
|
Java
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式。本文介绍了 Streams 的基本概念和使用方法,包括创建 Streams、中间操作和终端操作,并通过多个案例详细解析了过滤、映射、归并、排序、分组和并行处理等操作,帮助读者更好地理解和掌握这一重要特性。
6 2
|
1天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
5天前
|
存储 Java
判断一个元素是否在 Java 中的 Set 集合中
【10月更文挑战第30天】使用`contains()`方法可以方便快捷地判断一个元素是否在Java中的`Set`集合中,但对于自定义对象,需要注意重写`equals()`方法以确保正确的判断结果,同时根据具体的性能需求选择合适的`Set`实现类。
|
5天前
|
存储 Java 开发者
在 Java 中,如何遍历一个 Set 集合?
【10月更文挑战第30天】开发者可以根据具体的需求和代码风格选择合适的遍历方式。增强for循环简洁直观,适用于大多数简单的遍历场景;迭代器则更加灵活,可在遍历过程中进行更多复杂的操作;而Lambda表达式和`forEach`方法则提供了一种更简洁的函数式编程风格的遍历方式。
|
5天前
|
Java 开发者
|
16天前
|
SQL Java 关系型数据库
java连接mysql查询数据(基础版,无框架)
【10月更文挑战第12天】该示例展示了如何使用Java通过JDBC连接MySQL数据库并查询数据。首先在项目中引入`mysql-connector-java`依赖,然后通过`JdbcUtil`类中的`main`方法实现数据库连接、执行SQL查询及结果处理,最后关闭相关资源。
|
12天前
|
缓存 Java 数据库连接
Hibernate:Java持久层框架的高效应用
通过上述步骤,可以在Java项目中高效应用Hibernate框架,实现对关系数据库的透明持久化管理。Hibernate提供的强大功能和灵活配置,使得开发者能够专注于业务逻辑的实现,而不必过多关注底层数据库操作。
10 1
下一篇
无影云桌面