文章目录
- 1、迭代器(Iteration)
- 2、HashMap的工作原理
- 3、HashMap和Hashtable及ConcurrentHashMap都有什么区别
- 4、数组Array和列表集合ArrayList、LinkedList和Vector的区别
- 5、Comparable和Comparator接口是干什么的
- 6、什么是Java优先队列(PriorityQueue)
- 7、HashSet和TreeSet有什么区别
- 8、Java堆的结构是什么样子的,什么是队中永久代(Perm Gen Space)
1、迭代器(Iteration)
迭代器:是Java中的一种容器,迭代器(Iterator)是一个对象,它的工作就是遍历并选择序列中的对象,它提供了一种访问容器(container)对象中的各个元素,而又不必暴露该对象内部细节的方法(实现隔离解耦)。
容器:在Java集合框架的集合类,我们有时候称之为容器,比如;ArrayList、LinkedList、HashSet…每种容器都有自己的特点,ArrayList底层维护的是一个数组;LinkedList是链表结构的;HashSet依赖的是哈希表,每种容器都有自己特有的数据结构。
使用迭代器,开发人员不再需要了解容器底层的结构就可以实现对容器的遍历。而由于创建迭代器的代价非常小,因此迭代器也通常被称为轻量级的容器。
下面来看实现
在Iterator接口中定义了三个方法:
返回类型 | 方法 | 说明 |
boolean | hasNext() | 如果仍有元素可以迭代,则返回true |
E | next () | 返回迭代的下一个元素 |
void | remove() | 从迭代器指向的collection中移除迭代器返回的最后一个元素(可选操作) |
public static void main(String[] args) {
List<String> list=new ArrayList<>(); //创建放入数据
list.add("abc");
list.add("bca");
list.add("ccc");
Iterator<String> it=list.iterator(); //得到迭代器
while(it.hasNext()){ //使用it.hasNext()做判断条件
//System.out.println(it.next());
if("bca".equals(it.next())){ //获取元素比较
it.remove();
}
}
System.out.println(list);
}
截图
这里只是说明了一下迭代器的工作情况及使用
2、HashMap的工作原理
原理:HashMap是基于哈希表的map接口的非同步实现,此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
底层使用数组和链表的结合体
先贴上一张牛逼关系图
看下map
map就是用于存储键值对(<key,value>)的集合类,也可以说是一组键值对的映射(数学概念)。注意,我这里说的只是map的概念,是为了通俗易懂,面试时候方便记忆,但是你自己一定要明白,在java中map是一个接口,是和collection接口同一等级的集合根接口。
map特点:
1.没有重复的 key(一方面,key用set保存,所以key必须是唯一,无序的;另一方面,map的取值基本上是通过key来获取value,如果有两个相同的key,计算机将不知道到底获取哪个对应值;这时候有可能会问,那为什么我编程时候可以用put()方法传入两个key值相同的键值对?那是因为源码中,传入key值相同的键值对,将作为覆盖处理)
2.每个 key 只能对应一个 value, 多个 key 可以对应一个 value
3.key,value 都可以是任何引用类型(包括 null)的数据(只能是引用类型)
4.Map 取代了古老的 Dictionary 抽象类(知道就行,可以忽略)
HashMap
在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。
从上图中可以看出,HashMap底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。
hashMap特点:
- 底层实现是 链表数组,JDK 8 后又加了 红黑树
- 实现了 Map 全部的方法
- key 用 Set 存放,所以想做到 key 不允许重复,key 对应的类(一般是String)需要重写 hashCode 和 equals 方法
- 允许空键和空值(但空键只有一个,且放在第一位,知道就行)
- 元素是无序的,而且顺序会不定时改变(每次扩容后,都会重新哈希)
- 插入、获取的时间复杂度基本是 O(1)(前提是有适当的哈希函数,让元素分布在均匀的位置)
- 遍历整个 Map 需要的时间与数组的长度成正比(因此初始化时 HashMap 的容量不宜太大)
- 两个关键因子:初始容量、加载因子
- HashMap不是同步,HashTable是同步的,但HashTable已经弃用,如果需要线程安全,可以用synchronizedMap
HashMap 的 4 个构造方法
//创建一个空的哈希表,初始容量为 16,加载因子为 0.75
public HashMap()
//创建一个空的哈希表,指定容量,使用默认的加载因子
public HashMap(int initialCapacity)
//创建一个空的哈希表,指定容量和加载因子
public HashMap(int initialCapacity, float loadFactor)
//创建一个内容为参数 m 的内容的哈希表
public HashMap(Map<? extends K, ? extends V> m)
实例值得看一下
public class HashMapTest {
public static void main(String[] args) {
HashMap<String, String> map = new HashMap<String, String>();
// 键不能重复,值可以重复
map.put("san", "张三");
map.put("si", "李四");
map.put("wu", "王五");
map.put("wang", "老王");
map.put("wang", "老王2");// 老王被覆盖
map.put("lao", "老王");
System.out.println("-------直接输出hashmap:-------");
System.out.println(map);
/**
* 遍历HashMap
*/
// 1.获取Map中的所有键
System.out.println("-------foreach获取Map中所有的键:------");
Set<String> keys = map.keySet();
for (String key : keys) {
System.out.print(key+" ");
}
System.out.println();//换行
// 2.获取Map中所有值
System.out.println("-------foreach获取Map中所有的值:------");
Collection<String> values = map.values();
for (String value : values) {
System.out.print(value+" ");
}
System.out.println();//换行
// 3.得到key的值的同时得到key所对应的值
System.out.println("-------得到key的值的同时得到key所对应的值:-------");
Set<String> keys2 = map.keySet();
for (String key : keys2) {
System.out.print(key + ":" + map.get(key)+" ");
}
/**
* 另外一种不常用的遍历方式
*/
// 当我调用put(key,value)方法的时候,首先会把key和value封装到
// Entry这个静态内部类对象中,把Entry对象再添加到数组中,所以我们想获取
// map中的所有键值对,我们只要获取数组中的所有Entry对象,接下来
// 调用Entry对象中的getKey()和getValue()方法就能获取键值对了
Set<Map.Entry<String, String>> entrys = map.entrySet();
for (Map.Entry<String, String> entry : entrys) {
System.out.println(entry.getKey() + "--" + entry.getValue());
}
/**
* HashMap其他常用方法
*/
System.out.println("after map.size():"+map.size());
System.out.println("after map.isEmpty():"+map.isEmpty());
System.out.println(map.remove("san"));
System.out.println("after map.remove():"+map);
System.out.println("after map.get(si):"+map.get("si"));
System.out.println("after map.containsKey(si):"+map.containsKey("si"));
System.out.println("after containsValue(李四):"+map.containsValue("李四"));
//map.replace是jdk1.8方法
System.out.println(map.replace("si", "李四2"));
System.out.println("after map.replace(si, 李四2):"+map);
}
}
运行截图
3、HashMap和Hashtable及ConcurrentHashMap都有什么区别
HashTable
- 底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化
- 初始size为11,扩容:newsize = olesize*2+1
- 计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length
HashMap
- 底层数组+链表实现,可以存储null键和null值,线程不安全
- 初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂
- 扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
- 插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)
- 当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
- 计算index方法:index = hash & (tab.length – 1)
ConcurrentHashMap
- 底层采用分段的数组+链表实现,线程安全
- 通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
- Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
- 有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
- 扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容
Hashtable和HashMap都实现了Map接口,但是Hashtable的实现是基于Dictionary抽象类的。Java5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。
锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
4、数组Array和列表集合ArrayList、LinkedList和Vector的区别
ArrayList、Vector和LinkedList都是实现了List接口(允许数据重复)的容器类,它们都能对元素做增删改查的操作
一、ArrayList和Vector的区别
- 同步性: Vector是线程安全的,也就是说是同步的,而ArrayList是线程序不安全的,不是同步的。
- 操作性能:由于Vector支持多线程操作,所以在性能上就比不上ArrayList了。
- 数据增长:ArrayList和Vector都有一个初始的容量大小,当存储进去它们里面的元素个数超出容量的时候,就需要增加ArrayList和Vector的存储空间,每次增加存储空间的时候不是只增加一个存储单元,是增加多个存储单元。
Vector默认增加原来的一倍,ArrayList默认增加原来的0.5倍。
Vector可以由我们自己来设置增长的大小,ArrayList没有提供相关的方法。
二、LinkedList与ArrayList的区别
1、两者都实现的是List接口,不同之处在于:
(1)、ArrayList是基于动态数组实现的,LinkedList是基于链表的数据结构。
(2)、get访问List内部任意元素时,ArrayList的性能要比LinkedList性能好。LinkedList中的get方法是要按照顺序从列表的一端开始检查,直到另一端。
(3)、对于新增和删除操作LinkedList要强于ArrayList,因为ArrayList要移动数据。
LinkedList实现了List接口,允许null元素。此外LinkedList提供额外的get,remove,insert方法在LinkedList的首部或尾部。这些操作使LinkedList可被用作堆栈(stack),队列(queue)或双向队列(deque)。
注意LinkedList没有同步方法。如果多个线程同时访问一个List,则必须自己实现访问同步。一种解决方法是在创建List时构造一个同步的List:List list = Collections.synchronizedList(new LinkedList(…));
各有优缺点:
1.对ArrayList和LinkedList而言,在列表末尾增加一个元素所花的开销都是固定的。对
ArrayList而言,主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配;而对LinkedList而言,这个开销是统一的,分配一个内部Entry对象。2.在ArrayList的中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在LinkedList的中间插入或删除一个元素的开销是固定的。
3.LinkedList不 支持高效的随机元素访问。
4.ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间
总结使用说明:
1、当操作是在一列 数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList会提供比较好的性能;当你的操作是在一列数据的前面或中 间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList了。
2、所以,如果只是查找特定位置的元素或只在集合的末端增加、移除元素,那么使用Vector或ArrayList都可以。如果是对其它指定位置的插入、删除操作,最好选择LinkedList
三、数组Array和列表集合ArrayList的区别
- Array可以包含基本类型和对象类型,ArrayList只能包含对象类型。
- Array大小是固定的,ArrayList的大小是动态变化的。
- ArrayList提供了更多的方法和特性,比如添加全部addAll()、删除全部removeAll()、返回迭代器iterator()等。
- 对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢。
5、Comparable和Comparator接口是干什么的
Comparable (内比较器)& Comparator(外比较器) 都是用来实现集合中元素的比较、排序的,只是 Comparable 是在集合内部定义的方法实现的排序,Comparator 是在集合外部实现的排序,所以,如想实现排序,就需要在集合外定义 Comparator 接口的方法或在集合内实现 Comparable 接口的方法。
推荐参考:
6、什么是Java优先队列(PriorityQueue)
普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。优先队列具有最高级先出 (first in, largest out)的行为特征。通常采用堆数据结构来实现。
优先队列的实现:优先队列通常用堆来实现。因为通常所说的二叉堆(堆分为很多种:二叉堆,斐波拉契堆,严格斐波拉契堆等)每次插入或删除元素都会自动维持优先级的先后。
Java提供的PriorityQueue是一个基于小顶堆实现的优先队列,在这个队列中每次出操作都是返回的该队列中最小的那个元素。至于传入对象的方法怎么比较“大小”,一种是传入能自然排序的对象,例如:Integer、Long、Character、String等,如果是自定义类,则必须实现Comparator接口。
常用方法:
peek()//返回队首元素
poll()//返回队首元素,队首元素出队列
add()//添加元素
size()//返回队列元素个数
isEmpty()//判断队列是否为空,为空返回true,不空返回false
实例:TopK
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int K = scanner.nextInt();//需要维持最大值的数目
scanner.nextLine();
PriorityQueue<Integer> pq = new PriorityQueue<>();//优先队列(小顶堆)
while (true) {
int v = scanner.nextInt();
if (v == -1) {//输出-1表示停止输入,输出结果
System.out.println(pq.toString());
} else {
if (pq.size() < K) {
pq.add(v);
}else if(pq.peek()<v){
pq.poll();
pq.add(v);
}
}
}
}
}
推荐参考:
7、HashSet和TreeSet有什么区别
1、TreeSet 是二差树实现的,Treeset中的数据是自动排好序的,不允许放入null值。
2、HashSet 是哈希表实现的,HashSet中的数据是无序的,可以放入null,但只能放入一个null,两者中的值都不能重复,就如数据库中唯一约束。
3、HashSet要求放入的对象必须实现HashCode()方法,放入的对象,是以hashcode码作为标识的,而具有相同内容的 String对象,hashcode是一样,所以放入的内容不能重复。但是同一个类的对象可以放入不同的实例 。
4、线程都不安全
8、Java堆的结构是什么样子的,什么是队中永久代(Perm Gen Space)
JVM的堆是运行时数据区,所有类的实例和数组都是在堆上分配内存。它在JVM启动的时候被创建。对象所占的堆内存是由自动内存管理系统也就是垃圾收集器回收。
堆内存是由存活和死亡的对象组成的。存活的对象是应用可以访问的,不会被垃圾回收。死亡的对象是应用不可访问尚且还没有被垃圾收集器回收掉的对象。一直到垃圾收集器把这些对象回收掉之前,他们会一直占据堆内存空间。
永久代是用于存放静态文件,如Java类、方法等