目录
16.创建对象内存分析(栈、堆、方法区、PC寄存器、本地方法栈)
35.ArrayList通过无参构造器构造集合初始长度/ArrayList在存储时进行扩容的算法/原理
36.ArrayList与Vector区别(vector实现原理)
45.HashMap、Hashtable、ConcurrentHashMap区别
20210126
1.java有哪些特性
- 纯面向对象
- 跨平台
- 一种健壮的语言,吸收了 C/C++语言的优点
- 有较高的安全性。(自动回收垃圾,强制类型检查,取消指针)
2.什么是JVM?
- JVM 可以理解成一个可运行 Java 字节码的虚拟计算机系统.
- 它有一个解释器组件,可以实现 Java 字节码和计算机操作系统之间的通信.
- 对于不同的运行平台,有不同的JVM。
- JVM 屏蔽了底层运行平台的差别,实现了“一次编译,随处运行”。
3.Java跨平台原理
- java代码经编译器编译成字节码文件,字节码文件被jvm运行,jvm是万能翻译机,将字节码文件翻译给不同操作系统去执行。
4.什么是GC,为什么要有GC
- 在 C/C++等语言中,由程序员负责回收无用内存。
- Java 语言消除了程序员回收无用内存空间的责任: JVM 提供一种系统线程跟踪存储空间的分配情况。并在 JVM 的空闲时,检查并释放那些可以被释放的存储空间。
- 垃圾回收器在 Java 程序运行过程中自动启用,程序员无法精确控制和干预
- 有效的防止内存泄漏和使用内存。
5.Javase组成
6.JDK与JRE
- JDK(Java Development Kits)-- Java 开发工具集
- JRE(Java Runtime Environment)Java 运行时环境
- JDK主要包含:工具程序+API+JRE+JVM
- Java API (应用程序编程接口)、
- Java 编译器(javac.exe)、Java 运行时解释器(java.exe)、Java 文档化化工具(javadoc.exe)及其它工具及资源
- JRE JVM
- JRE 的三项主要功能:
- 加载代码:由类加载器(class loader)完成;
- 校验代码:由字节码校验器(byte code verifier)完成;
- 执行代码:由运行时解释器(runtime interpreter)完成。
7.Scanner的nextLine()方法
Scanner的nextLine()方法为什么有时候接收不到值? 为什么有时候控制台会跳过我的输入直接运行下面的代码?
- 其实Scanner对象是接收到了值的,否则也不会直接跳过输入过程。它接收到的是一个换行符(\n),是前面的next()、nextInt()等等这样的方法遗留下来的。
- Next()方法是按照空格来切分的,不会接收\n。想避免这种现象有两种方法:
- 1.使用nextLine()先接收一次上面遗留的\n,不必保留返回值,再使用String str=scanner.nextLine()保留返回值,接收真正有用的数据;
- 2.在使用nextLine()方法时就不要使用任何其他的next系列方法,需要其他类型都用转型方法+异常处理来解决。
- nextLine和其他的next方法有冲突,所有输入都使用nextLine,这样不会因为输入产生冲突,还可以接收各种类型数据(parsexxx类型转换)
8.switch使用注意事项
- switch有两个参数:
- ()中必须使用变量,类型必须是String、byte、short、int、char、枚举中的某一种类型,不能是long 类型。
- case后面跟着的必须是常量;
- switch仅适用于等值判断场合,发生区间判断的场合仅能使用if-elseif-else方式。
- switch中的break一般不能省略,省略后会发生case穿透,导致两个case中的代码同时进行,仅适用于确定想让多个case执行相同语句,或多个case的执行代码互相包含时才会选择性省略case,如月份日期的输出、文件更新补丁包的覆盖等等。
- default语句一般可以省略,适合需要对确定分支之外的所有情况进行判定时使用。
- 前面case定义的变量在后面的case中会发生同名冲突现象,解决方案:将每个case都封装成各自的方法或者单独加上大括号表示分隔。
9.简述java基本数据类型及内存空间
- 描述整数的数据类型主要有:byte/short/int/long,分别占用1个/2个/4个/8个字节大小。
- 描述小数的数据类型主要有:float/double,分别占用4个/8个字节大小。
- 描述真假的数据类型有:boolean,占用1个字节大小。
- 描述字符的数据类型有:char,占用2个字节。
10.&与&&区别
1.&和&&都可以用作逻辑与的运算符,表示逻辑与(and),全真则真,否则假。
2.&&还具有短路的功能,即如果第一个表达式为 false,则不再计算第二个表达式,例如,对于 if(str!=null &&!str.equals(“”))表达式,当 str 为 null 时,后面的表达式不会执行,所以不会出现 NullPointerException 如果将 &&改为&,则会抛出 NullPointerException 异常。If(x==33&++y>0)y 会增长,If(x==33&&++y>0)不会增长
3.&还可以用作位运算符,当&操作符两边的表达式不是 boolean 类型时,&表示按位与操作,我们通常使用 0x0f 来与一个整数进行&运算,来获取该整数的最低 4 个 bit 位,例如,0x31&0x0f 的结果为 0x01。
11.异常是否抛出,应该站在哪个角度思考?
- 如果是因为传参导致异常(非方法体问题),则通过throws抛出。
12.finally两个考点:必然执行和return时机
public class Demo6 {
public static void main(String[] args) {
Person p = haha();
System.out.println(p.age);
}
public static Person haha(){
Person p = new Person();
try{
p.age = 18;
return p;
}catch(Exception e){
return null;
}finally {
p.age = 28;
}
}
static class Person{
int age;
}
}
- 输出结果28,try中return p返回是备份好的返回值p,但p是引用类型指向内容,而finally中修改了p的内容(堆中),因此备份好的返回值也会改变。备份好的p还没被返回就被修改。如下图:
public class Demo7 {
public static void main(String[] args) {
int a = haha();
System.out.println(a);
}
public static int haha(){
int a = 10;
try{
return a;
}catch(Exception e){
}finally {
a = 20;
}
return 0;
}
static class Person{
int age;
}
}
- 输出结果10;try中return a返回的是备份好的a值,finally中修改的是栈中a的值,因为a不是引用类型,所以备份和修改的不是同一个变量。
package com.java.demo1;
public class Demo8 {
public static void main(String[] args) {
haha();
}
public static void haha(){
try{
int a = 10;
int b = 0;
System.out.println(a/b);
}catch(Exception e){
//退出JVM
System.out.println("出现了异常");
System.exit(0);
}finally {
System.out.println("锄禾日当午,汗滴禾下土");
}
}
}
- 程序输出"出现了异常"后就退出程序,没有执行finally,这是唯一不执行finally的情况System.exit(0)正常退出程序。
20210128
13.说说你对封装的理解
- 什么是封装:封装就是把类中的属性给上不同的访问权限,不允许外界随意修改。
- 为什么封装:保护类中的数据不被破坏
- 怎么封装:把类中的属性用不同的访问修饰符修饰,给外界提供可以操作的方法
14.面向过程到面向对象思想层面的转变
- 面向过程关注的是执行的过程,面向对象关注的是具备功能的对象。
- 面向过程到面向对象,是程序员思想上 从执行者到指挥者的转变。
15.类与对象关系
- 类表示一个共性的产物,是一个综合的特征,而对象,是一个个性的产物,是一个个体的特征。
- 类必须通过对象才可以使用,对象的所有操作都在类中定义。
- 类由属性和方法组成:
- 属性:就相当于人的一个个的特征 ·
- 方法:就相当于人的一个个的行为,例如:说话、吃饭、唱歌、睡觉
16.创建对象内存分析(栈、堆、方法区、PC寄存器、本地方法栈)
16.1.【栈】
16.1.1.Java栈存取数据特点?
- Java栈的区域很小 , 大概2m左右 , 特点是存取的速度特别快,, 先进后出。
16.1.2.栈存储速度快原因?
- 栈内存, 通过 '栈指针' 来创建空间与释放空间 !
- 指针向下移动, 会创建新的内存, 向上移动, 会释放这些内存 !
- 这种方式速度特别快 , 仅次于PC寄存器 !
16.1.3.为什么大部分数据存储到堆内存而不是栈?
- 栈使用栈指针进行数据存储,必须要明确移动的大小与范围 (需要关注开辟多少空间),
- 明确大小与范围是为了方便指针的移动 , 这是一个对于数据存储的限制, 存储的数据大小是固定的 , 影响了程序 的灵活性 ~ 所以我们把更大部分的数据 存储到了堆内存中
16.1.4.栈存储数据的类型
- 基本数据类型的数据 以及 引用数据类型的引用!
16.2.【堆】
16.2.1.堆存储数据的类型
引用类型
16.2.2.堆内存与栈内存不同
- 堆内存优点在于我们创建对象时 , 不必关注堆内存中需要开辟多少存储空间 , 也不需要关注内存占用 时长 !
- 堆内存中内存的释放是由GC(垃圾回收器)完成的
- 垃圾回收器 回收堆内存的规则:当栈内存中不存在此对象的引用时,则视其为垃圾 , 等待垃圾回收器回收 !
16.3.【方法区】
16.3.1.方法区可以存放哪些内容?
存放的是
- - 类信息
- - 静态的变量
- - 常量
- - 成员方法
方法区中包含了一个特殊的区域 ( 常量池 )(存储的是使用static修饰的成员)
16.4.【PC寄存器、本地方法栈】
- PC寄存器存放的内容
- PC寄存器保存的是 当前正在执行的 JVM指令的 地址 !
- 在Java程序中, 每个线程启动时, 都会创建一个PC寄存器 !
- 本地方法栈存放的内容
- 保存本地(native)方法的地址 !
17.static用法
- static表示“静态”的意思,可以用来修饰成员变量和成员方法(后续还会学习 静态代码块 和 静态内部类)。
- static的主要作用在于创建独立于具体对象的域变量或者方法
- 简单理解: 被static关键字修饰的方法或者变量不需要依赖于对象来进行访问,只要类被加载了,就可以通过类名去进行访 问。 并且不会因为对象的多次创建 而在内存中建立多份数据
- 1. 静态成员 在类加载时加载并初始化。
- 2. 无论一个类存在多少个对象 , 静态的属性, 永远在内存中只有一份( 可以理解为所有对象公用 )
- 3. 在访问时: 静态不能访问非静态 , 非静态可以访问静态 !内存中是【先】有静态内容,【后】有非静态内容,静态方法中不能使用this
18.权限修饰符使用范围
19.说说单例设计模式
- 单例设计模式:保证程序在内存中只有一个对象存在(被程序所共享)
- 单例设计模式的两种实现方式:
- 一、懒汉式:随着类的加载在内存中对象为null,当调用 getInstance 方法时才创建对象(延迟加载)
- 二、饿汉式:随着类的加载直接创建对象(推荐开发中使用)
- 单例设计模式的实现步骤:
- 1.保证一个类只有一个实例,实现方式:构造方法私有化
- 2.必须要自己创建这个实例,实现方式:在本类中维护一个本类对象(私有,静态)
- 3.必须向整个程序提供这个实例,实现方式:对外提供公共的访问方式(getInstance方法,静态)
懒汉式实现如下: class Single{ private Single(){} private static Single s1 = null; public static Single getInstance(){ if(s1 == null){ s1 = new Single(); } return s1; } } 饿汉式实现如下: class Single2{ private Single2(){} private static Single2 s = new Single2(); public static Single getInstance(){ return s; } void print(){ System.out.println("Hello World!"); } }
20.抽象类几个问题
- 抽象类能被实例化吗?
- 抽象类本身是不能直接进行实例化操作的,即:不能直接使用关键字new完成。
- 抽象类能否使用final声明?
- 不能,因为final属修饰的类是不能有子类的 , 而抽象类必须有子类才有意义,所以不能。
- 抽象类能否有构造方法?
- 能有构造方法,而且子类对象实例化的时候的流程与普通类的继承是一样的,都是要先调用父类中的构造方法(默 认是无参的),之后再调用子类自己的构造方法。
21.抽象类和普通类区别
- 1、抽象类必须用public或protected修饰(如果为private修饰,那么子类则无法继承,也就无法实现其抽象方法)。 默认缺省为 public
- 2、抽象类不可以使用new关键字创建对象, 但是在子类创建对象时, 抽象父类也会被JVM实例化。
- 3、如果一个子类继承抽象类,那么必须实现其所有的抽象方法。如果有未实现的抽象方法,那么子类也必须定义为 abstract类
22.面向接口编程优点
- 这种思想是接口是定义(规范,约束)与实现(名实分离的原则)的分离。
- 优点:
- 1、 降低程序的耦合性
- 2、 易于程序的扩展
- 3、 有利于程序的维护
23.接口包含哪些?什么时候将类定义成接口?
- 如果一个类中的全部方法都是抽象方法,全部属性都是全局常量,那么此时就可以将这个类定义成一个接口。
interface 接口名称{
全局常量 ;
抽象方法 ;
}
因为接口本身都是由全局常量和抽象方法组成 , 所以接口中的成员定义可以简写:
1、全局常量编写时, 可以省略public static final 关键字,例如:
public static final String INFO = "内容" ;---> String INFO = "内容" ;
2、抽象方法编写时, 可以省略 public abstract 关键字, 例如:
public abstract void print() ;--->void print() ;
24.抽象类与接口区别
- 1、抽象类要被子类继承,接口要被类实现。
- 2、接口只能声明抽象方法,抽象类中可以声明抽象方法,也可以写非抽象方法。
- 3、接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量。
- 4、抽象类使用继承来使用, 无法多继承。 接口使用实现来使用, 可以多实现
- 5、抽象类中可以包含static方法 ,但是接口中不允许(静态方法不能被子类重写,因此接口中不能声明静态方法)
- 6、接口不能有构造方法,但是抽象类可以有
25.多态的两种体现
- 多态:就是对象的多种表现形式,(多种体现形态)。在类中有子类和父类之分,子类就是父类的一种形态 ,对象多态性就从此而来。
- 多态可以分为两种:设计时多态和运行时多态。
-
设计时多态:即重载,是指Java允许方法名相同而参数不同(返回值可以相同也可以不相同)。
-
运行时多态:即重写,是指Java运行根据调用该方法的类型决定调用哪个方法。
- 重载: 一个类中方法的多态性体现
- 重写: 子父类中方法的多态性体现。
- 多态目的:增加代码的灵活度。
20210204
26.数据存储常用结构有哪些?
- 栈、队列、数组、链表和红黑树
27.使用栈结构的集合对元素的存取有什么特点?
- stack是限定仅在表尾进行插入和删除操作的线性表
- 先进后出
- 栈的入口、出口的都是栈的顶端位置。
28.使用队列结构的集合对元素存取有什么特点?
- queue只允许在表的 一端进行插入,而在另一端进行删除元素的线性表。只允许在表的 一端进行插入,而在另一端进行删除元素的线性表。
- 先进先出
- 队列的入口、出口各占一侧。
29.使用数组的集合对元素存储的特点?
- 查找元素快:通过索引,可以快速访问指定位置的元素
- 增删元素慢:增删都需要创建新数组,并将元素复制到新数组。
30.使用单链表的集合对元素存储特点?
- linked list,由一系列结点node(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。
- 每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的 指针域。
- 多个结点之间,通过地址进行连接。
- 查找元素慢:想查找某个元素,需要通过连接的节点,依次向后查找指定元素
- 增删元素快:
- 增加元素:只需要修改连接下个元素的地址即可。
- 删除元素:只需要修改连接下个元素的地址即可。
31.红黑树结构的集合存储元素特点
- 二叉树:binary tree ,是每个结点不超过2的有序树
- 红黑树本身就是一颗二叉查找树
- 红黑树的约束:
- 1. 节点可以是红色的或者黑色的
- 2. 根节点是黑色的
- 3. 叶子节点(特指空节点)是黑色的
- 4. 每个红色节点的子节点都是黑色的
- 5. 任何一个节点到其每一个叶子节点的所有路径上黑色节点数相同
- 红黑树的特点:
- 二分查找速度特别快,趋近平衡树,查找叶子元素最少和最多次数不多于二倍
32.类集设置的目的、java类集结构图
- 普通的对象数组的最大问题在于数组中的元素个数是固定的,不能动态的扩充大小,所以最 早的时候可以通过链表实现一个动态对象数组。但是这样做毕竟太复杂了,所以在 Java 中为了方便用户操作各个数据结构, 所以引入了类集的概念,有时候就可以把类集称为 java 对数据结构的实现。
33.Collection接口常用方法
- Collection 接口是在整个 Java 类集中保存单值的最大操作父接口,里面每次操作的时候都只能保存一个对象的数据。
- 在开发中不会直接使用 Collection 接口。而使用其操作的子接口:List、Set。为了更加清楚的区分,集合中是否允许有重复元素.
34.List接口常用方法
- 在 List 接口中有以上 10 个方法是对已有的 Collection 接口进行的扩充。,List 接口拥有比 Collection 接口更多的操作方法。
- ArrayList(95%)、Vector(4%)、LinkedList(1%)
- ArrayList和Vector都是基于数组动态扩容(新老数组复制)
- 在整个集合中 List 是 Collection 的子接口,里面的所有内容都是允许重复的。
35.ArrayList通过无参构造器构造集合初始长度/ArrayList在存储时进行扩容的算法/原理
ArrayList通过无参构造器构造集合初始长度
- 下面是api提供的ArrayList构造器初始容量为10,这其实是扩容后的值,最初始值其实是空数组{}.
ArrayList() | 构造一个初始容量为10的空列表。 |
---|---|
ArrayList(int initialCapacity) | 构造具有指定初始容量的空列表。 |
- 看下ArrayList构造器源码,点开elementData它是存储数据的数组,点开DEFAULTCAPACITY_EMPTY_ELEMENTDATA这个值是一个默认final的空数组,也就是说通过arraylist无参构造器构造的集合就是一个空数组,初始容量就是0.
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
transient Object[] elementData;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
arraylist扩容算法
- 创建list就是一个final修饰的空数组,容量0,当通过add方法添加元素时,点开add看下,无论添加成功与否返回的都是true,这是写死的。
ArrayList<Object> list = new ArrayList<>();
list.add(100);
- 里面执行添加的语句是add(e, elementData, size);传递三个参数,一个是待添加的元素e,一个是存数据的数组elementData,最后一个是有效数组实际长度size.
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
- 点开add(e, elementData, size)看看是如何进行添加元素?如果数组有效长度s等于数组长度length,则数组已满,调用grow()进行扩容并返回扩容后数组,否则直接将元素存入elementData中。
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
- grow()如何进行扩容?将目前数组长度(满长)加1作为数组最小容量传入到grow(int minCapacity)方法中,该方法中通过旧数组elementData和计算的新长度copyof出新数组给到elementData。新的长度通过newCapacity(minCapacity)方法计算出来,传参传的是最小需要的数组长度(原数组+1),
private Object[] grow() {
return grow(size + 1);
}
private Object[] grow(int minCapacity) {
return elementData = Arrays.copyOf(elementData,newCapacity(minCapacity));
}
- newCapacity(minCapacity)是如何计算新数组长度?先获取旧长度oldCapacity(数组存满时总长度),新长度newCapacity是旧长度+旧长度右移1位(即二进制右移一位即旧长度除以2)即旧长度的1.5倍。
- 下面做一个判断,若是新长度小于等于旧长度,说明新长度不够(比如未添加元素前初始长度是0,1.5倍还0;再比如添加一组数据,长度不可控时也是会进入判读)。
- 进一步判断若数组elementData是默认初始空数组{},则返回默认长度DEFAULT_CAPACITY(即10)与最小需要长度minCapacity(旧长度+1)两者最大值作为新长度,API中说的初始容量10就是新建的list第一次添加元素后容量。
- 另外当minCapacity长度超出int范围就会溢出,内存中二进制超出位数,然后由于符号的改变导致minCapacity变成负数,所以这里抛出一个内存溢出异常。
- 如果elementData不是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA即默认空数组{}即不是第一次添加元素又或者没有抛异常,则返回最小需要长度minCapacity即需要多少就多少刚好够存储.
- 若扩容以后新长度newCapacity够用,且新长度比允许的最大长度MAX_ARRAY_SIZE(即int类型最大值-8)小则返回新长度,若新长度比允许最大长度还大,则返回hugeCapacity(minCapacity)
private int newCapacity(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity <= 0) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
return (newCapacity - MAX_ARRAY_SIZE <= 0)? newCapacity: hugeCapacity(minCapacity);
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
- 该方法hugeCapacity(minCapacity)传递最小需要长度(旧长度+1),若minCapacity<0则内存溢出,若minCapacity大于允许的最大值MAX_ARRAY_SIZE则返回Int类型最大值Integer.MAX_VALUE,否则返回允许的最大值MAX_ARRAY_SIZE。
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE)? Integer.MAX_VALUE: MAX_ARRAY_SIZE;
}
总之:
- arraylist扩容就是根据旧数组和计算出的新长度copyof出新数组返回给elementData,arraylist初始是final修饰空数组长度0,当添加第一个元素时默认创建长度10的数组,之后扩容就是1.5倍进行扩容。
36.ArrayList与Vector区别(vector实现原理)
相同:
- 都实现List接口,ArrayList怎么存怎么取数据,Vector也是。都是通过size获取有效长度。
- 底层都是数组结构。
区别:
- Arraylist内部原理是默认情况下创建一个长度10的数组,Vector也是,但ArrayList每次扩容1.5倍,Vector可指定扩容长度。
- Vector与Collection实现不同,Vector是同步的,即是单线程,所以速度慢
- vector扩容算法和arraylist基本一样,只是在计算新长度时做了判断,若增量<0,则新长度为两倍旧长度,若增量>0则新长度为(旧长度+增量)
Vector<Integer> v = new Vector<>();
v.add(100);
Vector(int initialCapacity, int capacityIncrement) 构造具有指定初始容量和容量增量的空向量。
private int newCapacity(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity <= 0) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
return (newCapacity - MAX_ARRAY_SIZE <= 0)? newCapacity: hugeCapacity(minCapacity);
}
37.LinkedList 实现原理及使用
- LinkedList是一个双向链表,查询慢,增删快,
- LinkedList提供大量首尾操作的方法,通过addFirst(E e),addLast(E e),removeFirst(),removeLast()可模拟队列操作,通过push(E e),pop()模拟模拟堆栈操作
38.Iterator原理和使用
- 通用的取出集合中所有元素的一个接口,适用于任何集合。Iterator主要用于迭代访问(即遍历)Collection集合元素
原理
- 当遍历集合时,首先通过调用集合的iterator()方法获得迭代器对象,
- 然后使用hashNext()方法判断集合中是否存在下一个元素,如果存在,则调用next()方法将元素取出,否则说明已到达了集合末尾,停止遍历元素。
- Iterator迭代器对象在遍历集合时,内部采用指针的方式来跟踪集合中的元素,接下来通过一个图例来演示Iterator对象迭代元素的过程。在调用Iterator的next方法之前,迭代器的索引位于第一个元素之前,不指向任何元素,当第一次调用迭代器的next方法后,迭代器的索引会向后移动一位,指向第一个元素并将该元素返回,当再次调用next方法时,迭代器的索引会指向第二个元素并将该元素返回,依此类推,直到hasNext方法返回false,表示到达了集合的末尾,终止对元素的遍历。
使用
- 1.通过Collection集合中的方法iterator()获取迭代器的实现类对象,并使用Iterator接口接收。
- 2.使用Iterator接口中的方法hasNext判断还有没有下一个元素。
- 3.使用Iterator接口中的方法next取出集合中的下一个元素。
- 4.使用Iterator接口中的方法remove移除元素
39.Set接口
- Set 接口也是 Collection 的子接口,与 List 接口最大的不同在于,Set 接口里面的内容是不允许重复的。
- Set 接口并没有对 Collection 接口进行扩充,基本上还是与 Collection 接口保持一致。因为此接口没有 List 接口中定义 的 get(int index)方法,所以无法使用循环输出。
- 能通过循环的方式将 Set 接口中的内容输出,在 Collection 接口中定义了将集合变为对象数组进行输出Object obj[] = all.toArray(); // 将集合变为对象数组或者
- 在此接口中有两个常用的子类:HashSet、TreeSet
40.TreeSet与HashSet区别
- HashSet是散列存储无序,TreeSet是二叉树有序存储,这个有序不是存储有序而是根据排序规则进行排序,自定义元素需要实现comparable接口重写compartor方法定义自己排序规则。
- 关于 TreeSet 的排序实现,如果是集合中对象是自定义的或者说其他系统定义的类没有实现 Comparable 接口,则不能实现 TreeSet 的排序,会报类型转换(转向 Comparable 接口)错误。
- 换句话说要添加到 TreeSet 集合中的对象的类型必须实现了 Comparable 接口。 不过 TreeSet 的集合因为借用了 Comparable 接口,同时可以去除重复值,而 HashSet 虽然是 Set 接口子类,但是对于没有复写 Object 的 equals 和 hashCode 方法的对象,加入了 HashSet 集合中也是不能去掉重复值的。
41.Map、TreeMap、HashMap特点
- Map集合是一个双列集合,key是不允许重复的(key唯一),value可以重复。
- TreeMap 是Map子类,允许 key 自定义排序,其本身在操作时候将按照 key 进行排序,另外,key 存储自定义类型对象,要求对象所在的类必须实现 Comparable 接口。是从一般的开发角度来看,在使用 Map 接口的时候并不关心其是否排序,所以此类 只需要知道其特点即可。
- HashMap一个无序的集合,key唯一。
42.HashMap和Hashtable区别
- Hashtable:底层也是一个哈希表,是一个线程安全(单线程)的集合,因此速度慢。
- HashMap: 底层是一个哈希表(数组+链表/红黑树),是一个线程不安全(多线程)的集合,因此速度快。
43.哈希表如何存储元素
- 哈希表由数组+链表/红黑树组成,哈希表初始桶数量是16(即数组长度),散列因子(加载因子)是0.75(可自己设置),当桶的使用量达到75%,则对桶进行扩容2倍(就是数组扩容2倍),此时就是对32取余.
- 首先通过hashCode方法获取元素哈希值(int值),然后对16取余,余数作为数组索引,然后将元素存储对应位置中。
- 当出现哈希冲突(即哈希值取余结果相同),java采用链表和红黑树处理这个问题
- 当哈希桶中数据>8,则链表转换为红黑二叉树。
- 当哈希桶中数据减少到6,则红黑二叉树转换为链表。
- 若现在哈希桶中已知数据是7个,当减少数据后,不一定发生红黑树转换为链表,因为哈希桶可能本身就是链表。
44.HashMap源码分析
1.默认加载因子0.75
- 构造方法中设置默认的加载因子0.75
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; }
static final float DEFAULT_LOAD_FACTOR = 0.75f;
2.初始桶数量16
- 1左移4位即1乘以2的4次方
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
3.put方法存元素具体实现
- 先计算key的哈希值,然后将键和值作为参数传到putVal方法中存储元素
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
- 下面是putVal方法存储数据具体实现
- 1.首先调用put方法传入键值对,然后通过hash算法得到hash值,也就是下面代码
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
- 然后先判断(节点)数组table是否为Null或长度为0,是则调用resize()方法对table扩容。这里tab替代table,n表示扩容后长度。若不是则table没问题可存储数据。
if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;
- 既然table没问题,就可以通过hash值取余找到table下标,判断该位置是否已有元素(节点),没有则将元素以节点形式存入table,然后增加修改次数modcount++,然后对临界点进行比较,若达到散列的75%则调用resize扩容,否则结束。
if ((p=tab[i = (n - 1) & hash]) == null) tab[i]=newNode(hash, key, value, null);
若通过hash值找到的位置已经有元素,那么判断传入的key是否存在(重复),存在则新值覆盖旧值,若不存在则判断该table[i]节点是否是treeNode树节点,是则存入红黑二叉树,不是(即链表节点)则遍历链表,若链表长度>=8,则将该节点插入红黑树中,否则插入链表中。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//判断table是否正常
if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;
//table可存,通过hash值取余确定的位置为空,则直接添加节点元素
if ((p=tab[i =(n - 1)& hash]) == null) {tab[i]=newNode(hash, key, value, null);}
//若确定的位置已经有元素
else {
Node<K,V> e; K k;
//若table中取出的元素p的hash和待插入的hash相同并且key也相同,就出现key重复问题,
//将p赋值给e,然后就跳过了下面两个else,执行 if (e != null) 语句,e不空即代表
//table取出的元素p和待插入的元素key重复,所以进行value覆盖;若e为null则key不重复
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k)))){
e = p;
}else if (p instanceof TreeNode){
//若key不重复(即元素不重复),则判断table取出的元素若是树节点则存入红黑树
//若待存元素和红黑树中元素相同,则返回给e然后去覆盖,否则e还是null.
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
} else {
//若长度<8,则应存入链表中,先遍历链表,每一次遍历都要验证key是否重复,
//重复则直接跳出循环去执行if (e != null) 语句进行value覆盖,若不重复
//直接找到最后一个节点然后将待插节点插入其后。
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//当节点数>=(8-1)则转入二叉树
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
45.HashMap、Hashtable、ConcurrentHashMap区别
- HashMap线程不安全、但效率高(多人同时做同一件事(存取删),若存在共享数据则出现线程安全问题)
- Hashtable线程安全、效率低。(多人排队做一件事情)。
- ConcurrentHashMap采用分段锁机制,保证线程安全,效率较高。即锁住的范围只是一个桶,其他人可以对其它桶操作。Hashtable锁住的是整个集合。
46.散列表散列操作
- 影响HashMap性能的参数:初始容量、负载(散列)因子。
- 若初始容量给的小,但存储的数据很多,哈希表就会频繁的散列,即重建内部数据结构即重新建立2倍大小哈希表,将原数据再一一存进去,这样导致最后存储数据次数是原来多倍,造成性能浪费。
- 加载因子若给的小,则占用大量内存空间但效率高,若过大则越节省空间但查询效率过低。
47.哈希值错乱问题
- 当一个对象作为哈希表的键存储时,这个对象就不要进行修改,不然重新计算的哈希值与原来哈希值会不一样,就找不到原数据,造成内存泄漏。
- 只要是以哈希表存储元素,被计算哈希值的字段内容就不要修改,若一定要修改,这个字段就不要放在键的位置,当然这是对map来说,若是对set来说只能存单列,是一定不能修改的。
- 比如当一个对象被存进 HashSet 集合后,就不能修改这个对象中的那些参与计算的哈希值的字段了,否则,对象被修改后的哈 希值与最初存储进 HashSet 集合中时的哈希值就不同了,在这种情况下,即使在 contains 方法使用该对象的当前引用作为 的参数去 HashSet 集合中检索对象,也将返回找不到对象的结果,这也会导致无法从 HashSet 集合中删除当前对象,从而 造成内存泄露。