Java笔记:集合
集合是一种容器对象,是用来存储对象的,或者说存储的都是对象的引用,并不是直接存储的对象,而是存储的对象的内存地址。需要注意的是,集合中不能存储基本数据类型,即使是代码中往集合中添加了基本数据类型,那也是进行了自动装箱之后才放入集合的。 需要注意的是,Java中每一种不同的集合,底层会对应不同的数据结构,所以应该根据实际情况选择使用合适的集合类型。 所有的集合都在“java.util”中,导入的时候去util包里找即可。 注:集合的定义中会经常看到<E>等尖括号表示的语法,这是泛型的定义,表示可以根据需要传入自己需要的类型,如果不传入,默认就是Object类型。即如果传入指定的类型,则集合中只能存储指定的类型,否则集合中可以存储Object类的所有子类型。 ? 1、Collection集合继承结构 Collection类型的集合特点是以单个对象的个体方式存储元素。 java.lang.Object --> java.util.AbstractCollection<E>这个类实现了以下两个接口:
Collection<E>接口中的常用方法:
Iterator<E> iterator()中常用的方法:
Collection<E>接口下常用的子接口:
List<E>接口中的常用方法:
? 2、Map集合继承结构 Map类型的集合特点是以key和value键值对的方式来存储集合元素的,即一个键值对算作一个集合元素。 java.util.Map<K,?V>接口中常用的方法:
java.util.Map<K,?V>接口的常用实现类:
? 3、ArrayList<E> 特点:集合元素有序可重复,可通过下标访问集合元素,并且是非线程安全的。 方法:Collection<E>和List<E>接口中的方法ArrayList<E>都可以使用。 线程安全:如果想要将ArrayList<E>变为线程安全的,可以通过“java.util.Collections”的synchronizedList(yourList)方法将你的ArrayList对象传入,之后的ArrayList就会变成线程安全的了。 底层原理:ArrayList底层采用的是Object类型数组的数据结构,创建对象时可以传入数组的大小,如果没有传入数组大小,则会在初始化时创建一个空列表,然后在第一次往里面添加数据时才会创建一个大小为10的数组。在往数组中添加数据时,如果容量满了,那么数组会自动进行扩容,并且扩容之后的数组容量为原容量的1.5倍。 优缺点:就和数组的优缺点一样,查询效率很快,往末尾添加数据也很快,但是随机增删元素的效率较低,所以使用的使用应该尽量避免随机增删以及扩容的操作。 使用示例:? import java.util.ArrayList; java.util.Iterator; java.util.List; public class GenericTest { static void main(String[] args) { // 指定集合中的元素类型为Pet,不能存储其它类型的元素 使用new的时候可以不用再传入类型了,可以自动推断,此时的表达式<>也称为钻石表达式 如果不指定泛型,也是可以的,默认就是Object类型 List<Pet> petList = new ArrayList<>(); Cat c = new Cat(); Dog d = Dog(); petList.add(c); petList.add(d); 迭代器的声明也需要加上泛型的定义 Iterator<Pet> it = petList.iterator(); while (it.hasNext()) { 原本next方法返回值的类型为Object,使用泛型之后返回的类型直接就是指定 的类型,不需要进行类型转换了。 Pet p = it.next(); p.play(); 当然,如果要使用具体的子类对象的方法,还是需要转型之后才能调用 if (p instanceof Cat){ Cat myCat = (Cat)p; myCat.sleep(); } Dog){ Dog myDog = (Dog)p; myDog.bark(); } } /* 输出结果: 宠物在玩耍! 猫咪在睡觉! 宠物在玩耍! 狗子在嚎叫! */ } } Pet { play() { System.out.println("宠物在玩耍!"); } } class Cat extends sleep() { System.out.println("猫咪在睡觉!"class Dog bark() { System.out.println("狗子在嚎叫!"); } } ? ? 4、LinkedList<E> 特点:集合元素有序可重复,可通过下标访问集合元素。 方法:Collection<E>和List<E>接口中的方法ArrayList<E>都可以使用。 底层原理:底层采用的是双向链表的数据结构,存储的时候会将元素封装到一个Node对象中,这个Node对象除了有元素本身外,还有两个变量next和prev,用于下一个节点和上一个节点的地址,这样就形成了一个“双向”的链表了。虽然是链表,但是还是可以使用下标,但是不推荐使用下标进行查询,因为每次查询都得从头结点开始,效率较低。 优缺点:查询效率较低,因为每次都要从头结点开始查询,但是随机增删元素的效率较高,因为只会涉及到上一个节点和下一个节点中变量next和prev的修改,不会像数组那样会涉及到该元素之后的值的整体移动。所以在选择使用上,如果随机增删的业务较多,应该使用LinkedList,如果需要往集合中频繁的添加或查询数据,就使用ArrayList。 容量:LinkedList因为是链表的数据结构,所以在内存空间上是没有连续性的,并且也没有容量的说法,刚开始都是空的,没有任何元素。 ? 5、Vector<E> Vector和ArrayList一样底层都是数组的数据结构,在使用上也是一样的,区别在于Vector是线程安全的,但是也是因为这点,导致效率较低,而ArrayList可以使用其他更好的方法来保证线程安全,所以Vector已经使用的较少了。 ? 6、HashSet<E> 特点:集合元素无序不可重复,不可通过下标访问集合元素。 方法:Collection<E>接口中的方法ArrayList<E>都可以使用。 底层原理:HashSet底层其实是采用HashMap类型来存储元素的,不过只是用到了HashMap的key部分,value部分并没有用到,HashMap的key本身就是无序不可重复的,所以可以看成是HashMap所有的Key组成了一个HashSet。所以想要更好的理解HashSet,参见HashMap部分的笔记。 ? 7、TreeSet<E> 特点:集合元素无序不可重复,不可通过下标访问集合元素,但是存放到集合中的数据都是按升序自动排好序的。 方法:Collection<E>接口中的方法ArrayList<E>都可以使用。 底层原理:由于TreeSet实现了SortedSet,所以存放进去的元素都是排好序的,“无序不可重复”中的无序指的是存入进去的顺序和取出来的顺序不一致,这和存入进去之后自动排序是不一样的,不要搞混了。和HashSet类似,TreeSet底层其实使用Map下的TreeMap来存储数据的,它也是只用到了TreeMap的key部分,没有用到value部分,所以理解TreeSet,可以参见TreeMap部分的笔记。 自动排序:为了改变自动升序为自动降序排列,或者存储自定义的类型的时候,需要重写或自定义TreeSet的排序机制,特别是自定义的类型,如果没有同时定义好排序机制的话是会报错的,自定义排序机制为:给这个自定义的类实现“java.lang.Comparable”接口或者自定义一个比较器“java.util.Comparator”。示例如下: java.util.Comparator; java.util.TreeSet; TreeSetTest { main(String[] args) { User u1 = new User(10); User u2 = new User(30); User u3 = new User(20); 默认使用无参构造方法,即比较器为null,此时需要在类中实现Comparable接口的compareTo方法 TreeSet<User> ts = new TreeSet<>(); 也可以自定义一个比较器,传入构造方法 TreeSet<User> ts = new TreeSet<>(new UserComparator()); ts.add(u1); ts.add(u2); ts.add(u3); for (User u : ts){ System.out.println(u); } } } 如果是需要往TreeSet中添加,则需要实现Comparable接口,并重写compareTo方法 */ class User implements Comparable<User>{ int age; public User(){ } public User( age){ this.age = age; } 排序会根据compareTo方法返回大于0、小于0或者等于0三种情况进行排序 可根据自身需要根据三种结果进行自定义排序的规则的编写 */ compareTo(User u){ return this.age - u.age; } String toString(){ return "User: " + age; } } 构造器需要实现Comparator接口,并重写compare方法 class UserComparator implements Comparator<User> compare(User u1,User u2){ return u1.age - u2.age; } } ? 8、HashMap<K,?V> ?特点:使用键值对存储数据,key无序不可重复,value是可以重复的,并且是非线程安全的。 底层原理:HashMap的底层其实是哈希表或者说散列表的数据结构,而这个哈希表又是由数组和单向链表或者红黑树组成,初始化时它是单向链表,当添加的元素导致单向链表的节点数超过8个时,就会自动转换为红黑树,而红黑树的节点数如果少于了6个,又会自动转换为单向链表,之所以这样实现,还是想要更加快速地对集合元素进行查询。数组部分其实是一个Node类型的一位数组,每个元素都是一个Node对象,Node对象有“final int hash”、“final K key”、“V value”、“Node<K,V> next”四个基本属性,其中hash表示是将key通过从Object重写的hashCode方法返回的哈希值,并且这个hash值在底层可以通过另外的哈希算法(这个算法不需要关注)得到对应的数组下标,即这个hash值可以看成是和数组下标对应的,或者直接将他看成数组下标也行,next则表示下一个节点的内存地址。以最开始的数组和单向链表为例,使用put方法和get方法更加详细的说明HashMap的原理。 put方法原理:当执行put方法将一个键值对存入HashMap集合时,会按照以下步骤执行:
get方法原理:当使用一个key值通过get方法获取对应的value时,会按照以下步骤执行:
hashCode()和equals()方法重写:由以上put和get的实现原理可以看出,同一个数组位置的单向链表上的所有节点的hash值都是相同的,但是key的equals方法返回值肯定是不同的。同时也可以发现,使用HashMap时,key对象需要重写两个方法,hashCode()和equals()方法。重写hashCode方法时需要注意以下内容:
数组容量:HashMap集合的默认容量是16,默认加载因子是0.75,表示默认的数组长度为16,当实际的数组使用达到容量的75%时就会进行自动扩容,扩容之后的容量是原容量的2倍。也可以在初始化时指定容量大小,但是官方推荐初始化容量最好是2的倍数,不然会影响HashMap的执行效率。 使用示例: java.util.HashMap; java.util.Map; java.util.Set; MapTest { main(String[] args) { Map<Integer,String> map = new HashMap<>(); map.put(1,"张三"); map.put(2,"李四"); 通过key遍历Map中的键值对 Set<Integer> keys = map.keySet(); (Integer key : keys) { String value = map.get(key); System.out.println(key + "=" + value); } 通过Set<Map.Entry<Integer,String>>直接遍历Map中的键值对 如果内存允许,这种方式效率会比较高,适合大数据量的遍历 Set<Map.Entry<Integer,String>> es = map.entrySet(); for (Map.Entry<Integer,String> node : es){ System.out.println(node.getKey() + "=" + node.getValue()); } } } ? 9、Hashtable<K,?V> 和Properties Hashtable和HashMap的使用相似的,区别在于Hashtable是线程安全的,而后者是非线程安全的,但是和Vector的情况一样,Hashtable线程安全的实现方式导致了效率较低的问题,所以使用的较少了。 Hashtable的一个子类Properties相比而言要常用一点,Properties也是线程安全的,Properties对象也称之为属性对象,它的特点是key和value都只支持String类型。常用的方法有:
注:Properties的在“Java笔记:IO流” 笔记的最后“10. io和Properties联合使用以读取配置文件”有一个使用示例,可以参考下。 ? 10、TreeMap<K,?V> 特点:使用键值对存储数据,key无序不可重复,value是可以重复的,并且存入之后会按key升序自动排序。 底层原理:TreeMap实现了SortedMap<K,?V>接口,并且底层其实是自平衡二叉树的数据结构,这种数据结构遵循左小右大的原则存放数据。存入的key和value其实是封装到了一个Entry<K,V>节点对象中,这个节点对象除了存入的key和value,还有三个变量left、right和parent用于存放左子节点、右子节点和父节点的内存地址,这样就刚好形成一个二叉树。遍历这个二叉树时,通常有三种方式:前序遍历(根左右)、中序遍历(左根右)和后序遍历(左右根),这个“前中后”指的是根(节点)相对于“左右”的位置或者说遍历中的顺序,对于中序遍历,特点是遍历出来的数据自动就是从小到大排序的。 ? 11、Collections工具类 这个类专门定义了一些方便集合操作的方法。常用的有:
? (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |