对象数组
- 数组既可以存储基本数据类型,也可以存储引用类型。它存储引用类型的时候的数组就叫对象数组。
- 比如:用数组存储5个学生对象,并遍历数组。
集合
集合由来
我们学习的是Java -- 面向对象 -- 操作很多对象 -- 存储 -- 容器(数组和StringBuffer) -- 数组
而数组的长度固定,所以不适合做变化的需求,Java就提供了集合供我们使用。
集合和数组的区别
- 长度区别
数组固定
集合可变 - 内容区别
数组可以是基本类型,也可以是引用类型
集合只能是引用类型 - 元素内容
数组只能存储同一种类型
集合可以存储不同类型(其实集合一般存储也是同一种类型)
集合关系图
image.png注意:
数组中 length(属性),字符串length()方法,集合size()方法
List集合遍历 (5种方式)
// 创建集合
List<String> list = new ArrayList<>();
// 添加元素
for (int i = 0; i < 1000; i++) {
list.add(UUID.randomUUID().toString());
}
// 遍历
// 普通遍历
long startTime = System.currentTimeMillis();
for (int i = 0, length = list.size(); i < length; i++) {
System.out.print(list.get(i));
}
System.out.println();
System.out.println("普通遍历花费时长=: " + (System.currentTimeMillis() - startTime)); // 10ms
startTime = System.currentTimeMillis();
// 增强for循环遍历
for (String str : list) {
System.out.print(str);
}
System.out.println();
System.out.println("增强for循环遍历花费时长=: " + (System.currentTimeMillis() - startTime)); // 3ms
startTime = System.currentTimeMillis();
// 迭代器遍历
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.print(iterator.next());
}
System.out.println();
System.out.println("迭代器遍历花费时长=: " + (System.currentTimeMillis() - startTime));// 4ms
startTime = System.currentTimeMillis();
// 流遍历
list.stream().forEach(System.out::print);
System.out.println();
System.out.println("流遍历花费时长=: " + (System.currentTimeMillis() - startTime));// 48ms
startTime = System.currentTimeMillis();
// 并行流遍历
list.parallelStream().forEach(System.out::print);
System.out.println();
System.out.println("并行流遍历花费时长=: " + (System.currentTimeMillis() - startTime));// 12ms
Iterator源码
- 为什么定义了一个接口而不是实现类?
- 迭代器内部实现
public interface Iterator<E> {
boolean hasNext();
Object next();
}
public interface Iterable<T> {
Iterator<T> iterator();
}
public interface Collection<E> extends Iterable<E> {
Iterator<E> iterator();
}
public interface List<E> extends Collection<E> {
Iterator<E> iterator();
}
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator {
public boolean hasNext() {}
public Object next(){}
......
}
}
Collection集合总结
Collection:
- List 有序,可重复
- ArrayList
底层数据结构是数组,查询快,增删慢
线程不安全,效率高 - Vector
底层数据结构是数组,查询快,增删慢
线程安全,效率低 - LinkList
底层数据结构是链表,查询慢,增删快
线程不安全,效率高
- ArrayList
- Set 无序,唯一
- HashSet
底层数据结构是哈希表
如何保证元素的唯一?
依赖两个方法: hashCode()和equals()
开发中自动生成这两个方法即可 - LinkedHashSet
底层数据结构是链表和哈希表
由链表保证元素有序
由哈希表保证元素唯一 - TreeSet
底层数据结构是红黑树
如果保证元素排序呢?
自然排序
比较器排序
如何保证元素唯一性?
根据比较的返回值是否是0来决定
- HashSet
集合Map
键值对,键不能重复,值可以重复
Map和Collection 的区别
- Map存储的是键值对形式的元素,键唯一,值可以重复。夫妻对
- Collection 存储的是单独出现的元素,子接口Set元素唯一,子接口List元素可重复。光棍
HashMap:是基于哈希表的Map接口实现。
哈希表的作用是用来保证键的唯一性。
ThreeMap:是基于红黑树Map接口的实现。
Map集合遍历(三种方式)
Map<Integer, String> map = new HashMap<>();
// 添加数据
for (int i = 0, size = 1000; i < size; i++) {
map.put(i, UUID.randomUUID().toString());
}
map.isEmpty();
long startTime = System.currentTimeMillis();
// 遍历
// 方式一:键找值
Set<Integer> integerSet = map.keySet();
for (Integer key : integerSet) {
String value = map.get(key);
System.out.print(key + "---" + value + " ");
}
System.out.println();
System.out.println("花费时间= :" + (System.currentTimeMillis() - startTime)); // 11ms
startTime = System.currentTimeMillis();
// 方式二:键值对对象找键和值
Set<Map.Entry<Integer, String>> set = map.entrySet();
for (Map.Entry<Integer, String> son : set) {
Integer key = son.getKey();
String value = son.getValue();
System.out.print(key + "---" + value + " ");
}
System.out.println();
System.out.println("花费时间= :" + (System.currentTimeMillis() - startTime));// 5ms
startTime = System.currentTimeMillis();
// 方式三:JAVA8
map.forEach((k, v) -> {
System.out.print(k + "---" + v + " ");
});
System.out.println();
System.out.println("花费时间= :" + (System.currentTimeMillis() - startTime)); //45ms
泛型
-
概念
是一种把明确类型的工作推迟到创建对象或者调用方法的时候才去明确的特殊类型 -
格式
<数据类型>
注意:该数据类型只能是引用类型
泛型类:
class 类名<声明自定义的泛型>{...}- 在类上自定义的泛型的具体数据类型是在创建对象的时候指定的。
- 在类上自定义了泛型,如果创建该类的对象时没有指定泛型的具体类型,那么默认是Object类型。
泛型方法
修饰符 <声明自定义泛型>返回值类型 函数名(形参列表..){......} - 在方法上的自定义泛型的具体数据类型是调用该方法的时候传入实参的时候确定的。
- 自定义泛型使用的标识符只要符合标识符的命名规则即可。
需求: 定义一个函数可以接收任意类型的参数,要求函数的返回值类型与实参的数据类型要一致。
public static <abc> abc print(abc o){
return o;
}
泛型接口
interface 接口名<声明自定义的泛型>{......}
泛型高级通配符
?
? extends E
? super E
泛型一般在集合中使用
-
优点:
- 把运行时期的问题提前到了编译时期
- 避免了强类型转换
- 优化了程序设计,解决了黄色警告线问题,让程序更安全
-
面试题
什么是泛型?为什么要使用以及泛型擦除?
泛型,即参数化类型
创建集合的时候就制定集合元素的类型,该集合只能保存其指定类型的元素,避免使用强制类型转换。
JAVA编译器生成的字节码是不包含泛型信息的,泛型信息将在编译处理时被擦除,这个过程即类型擦除。泛型擦除可以简单理解为将泛型java代码转换为普通java代码,只不过编译器更直接些,将泛型java代码直接转换成普通java字节码。
类型擦除的主要过程如下:- 将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。
- 移除所有的类型参数
集合阿里规约
- 【强制】 关于 hashCode 和 equals 的处理,遵循如下规则:
- 只要重写equals,就必须重写hashcode;
- 因为Set存储的是不重复的对象,依据hashCode和equals进行判断,所以Set存储的对象必须重写这两个方法。
- 如果自定义对象作为Map的键,那么必须重写hashCode和equals。
说明:String重写了hashCode和equals方法,所以我们可以非常愉快地使用String对象作为key来使用。
- 【强制】 ArrayList的subList结果不可强转成ArrayList,否则会抛出ClassCastException
异常, 即 java.util.RandomAccessSubList cannot be cast to java.util.ArrayList.
说明: subList 返回的是 ArrayList 的内部类 SubList,并不是 ArrayList ,而是
ArrayList 的一个视图,对于 SubList 子列表的所有操作最终会反映到原列表上。 - 【强制】在 subList 场景中, 高度注意对原集合元素个数的修改,会导致子列表的遍历、增加、
删除均会产生 ConcurrentModificationException 异常。 -
【强制】 使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一
致、长度为 0 的空数组。
反例:
直接使用 toArray 无参方法存在问题,此方法返回值只能是 Object[]类,若强转其它类型数组将出现ClassCastException 错误。
正例:
List<String> list = new ArrayList<>(2);
list.add("guan");
list.add("bao");
String[] array = list.toArray(new String[0]);
说明:使用 toArray 带参方法,数组空间大小的 length
1. 等于 0,动态创建与 size 相同的数组,性能最好。
2. 大于 0 ,但小于 size,重新创建大小等于 size 的数组,增加 GC 负担。
3. 等于 size,在高并发情况下,数组创建完成之后, size 正在变大的情况下,负面影响与 2 相同。
4. 大于 size,空间浪费,且在 size 处插入 null 值,存在 NPE 隐患。
- 【强制】 不要在 foreach 循环里进行元素的 remove/add 操作。 remove 元素请使用 Iterator
方式,如果并发操作,需要对 Iterator 对象加锁。
正例:
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (删除元素的条件) {
iterator.remove();
}
}
反例:
for (String item : list) {
if ("1".equals(item)) {
list.remove(item);
}
}
说明: 以上代码的执行结果肯定会出乎大家的意料,那么试一下把“1” 换成“2” ,会是同样的结果吗?
-
【推荐】集合初始化时,指定集合初始值大小。
说明:HashMap使用HashMap(int initialCapacity) 初始化,如果暂时无法确定集合大小,那么指定默认值(16)即可。
正例:initialCapacity = (需要存储的元素个数/负载因子)+1。注意负载因子(即 loader factor) 默认为 0.75,如果暂时无法确定初始值大小,请设置为 16(即默认值) 。
反例:HashMap需要放置1024个元素,由于没有设置容量初始大小,随着元素不断增加,容量7次被迫扩大,resize需要重建hash表。当放置的集合个数级别达千万级别时,不断扩容会严重影响性能。 - 【推荐】使用entrySet遍历Map类集合k v,而不是keySet方式进行遍历。
说明:keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出 key 所对应的value。而 entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中,效率更高。如果是 JDK8,使用Map.forEach 方法。
正例:values()返回的是V值集合,是一个list集合对象;keySet()返回的是K值集合,是一个Set集合对象;entrySet()返回的是K-V值组合集合。 - 【推荐】 高度注意 Map 类集合 K/V 能不能存储 null 值的情况,如下表格:
集合类 | Key | Value | Super | 说明 |
---|---|---|---|---|
Hashtable | 不允许为null | 不允许为null | Dictionary | 线程安全 |
ConcurrentHashMap | 不允许为null | 不允许为null | AbstractMap | 锁分段技术( JDK8:CAS) |
TreeMap | 不允许为null | 允许为null | AbstractMap | 线程不安全 |
HashMap | 允许为null | 允许为null | AbstractMap | 线程不安全 |
反例: 由于 HashMap 的干扰,很多人认为 ConcurrentHashMap 是可以置入 null 值,而事实上, 存储null 值时会抛出 NPE 异常。
网友评论