字符串
String的创建机理是什么?什么是字符串常量池?
创建机理:由于String在Java世界中使用过于频繁,为了提高内存的使用率,避免开辟多块空间存储相同的字符串,引入了字符串常量池(字符串常量池位于堆内存中)。
其运行机制是:在创建字符串时 JVM 会首先检查字符串常量池,如果该字符串已经存在池中,则返回它的引用,如果不存在,则实例化一个字符串放到池中,并返回其引用。
*Math的四舍五入
四舍五入方法原理是先将传入的参数加0.5然后再向下取整
Switch可作用哪些数据类型
可以用在byte、short、char、int、枚举类Enum(java5引入)、String(java7引入)
抽象类和接口的区别
抽象类是向上(抽象),接口是向下(规范)
*内部类传入参数限制
局部内部类和匿名内部类访问外部方法的局部变量时该变量必须是final的,原因是局部变量和类的生命周期不一样,这就可能出现一个问题:局部类去访问一个可能已经在JVM中消失了的局部变量,所以java采取了一个不那么复杂的处理办法就是将局部变量复制一份传入内部类,又因为对该复制变量的修改同步到原变量很难实现(代价极高),所以将其设置成final
异常(Throwable)
错误(Error)
Error 类是指 java 运行时系统的内部错误和资源耗尽错误。应用程序不会抛出该类对象。如果出现了这样的错误,除了告知用户,剩下的就是尽力使程序安全的终止。
异常(Exception)
编译时(受检)异常
编译时抛出,需要使用try-catch捕获或者throws向上抛出,如IOException
运行时异常(RuntimeException)
程序运行时抛出的异常,如NullPointerException空指针异常,数学运算异常(除数为0等)
*JVM 处理异常流程
在一个方法中如果发生异常,这个方法会创建一个异常对象,并转交给 JVM,该异常对象包含异常名称,异常描述以及异常发生时应用程序的状态。创建异常对象并转交给 JVM 的过程称为抛出异常。可能有一系列的方法调用,最终才进入抛出异常的方法,这一系列方法调用的有序列表叫做调用栈。
JVM 会顺着调用栈去查找看是否有可以处理异常的代码,如果有,则调用异常处理代码。如果 JVM 没有找到可以处理该异常的代码块,JVM 就会将该异常转交给默认的异常处理器(默认处理器为 JVM 的一部分),默认异常处理器打印出异常信息并终止应用程序。
*从性能角度来审视一下 Java 的异常处理机制
- try-catch 代码段会产生额外的性能开销,或者换个角度说,它往往会影响 JVM 对代码进行优化,所以建议仅捕获有必要的代码段,尽量不要一个大的 try 包住整段的代码;
- 利用异常控制代码流程,也不是一个好主意,远比我们通常意义上的条件语句(if/else、switch)要低效;
- Java 每实例化一个 Exception,都会对当时的堆栈进行快照,这是一个相对比较重的操作。如果发生的非常频繁,这个开销可就不能被忽略了。
Java常见异常有哪些
Error
- java.lang.OutOfMemoryError:内存不足错误。当可用内存不足以让Java虚拟机分配给一个对象时抛出该错误。
- java.lang.StackOverflowError:堆栈溢出错误。当一个应用递归调用的层次太深而导致堆栈溢出或者陷入死循环时抛出该错误。
Exception
- IOException(IO异常)
RuntimeException
- NullPointerException(空指针异常)
- ClassCastException(类转换异常)
- IndexOutOfBoundsException(索引越界异常)
反射
获取类对象的方式
- 类名.class 如String.class
- 对象.class 如“a”.getClass()
- Class.forName(“类的全路径”),如 Class.forName(“java.lang.String”)
使用类对象创建实例对象
- 类对象.newInstance()
- 类对象.getDeclaredConstructor(传入参数类型的类对象)获取指定的构造器对象Constructor,然后再用构造器对象.newInstance(传入参数)
将获取到的私有字段或者私有方法变得可访问
setAccessible(true)
调用对象方法
getMethod(方法名),调用method.invoke(obj, arg)
*反射为什么慢
- 反射调用过程中会产生大量的临时对象,这些对象会占用内存,可能会导致频繁 gc,从而影响性能。
- 反射调用方法时会从方法数组中遍历查找,并且检查可见性等操作会比较耗时。
- 反射在达到一定次数时,会动态编写字节码并加载到内存中,这个字节码没有经过编译器优化,也不能享受 JIT 优化。
- 反射一般会涉及自动装箱/拆箱和类型转换,都会带来一定的资源开销。
注解
元注解
@Target表示注解作用的对象范围
@Retention表示作用的时长
- SOURCE:在源文件中有效(即源文件java保留)
- CLASS:在 class 文件中有效(即 class 保留)
- RUNTIME:在运行时有效(即运行时保留)
@Documented生成javadoc时将有该注解
@Inherited表示该注解可以被继承,如父类标记了该注解,则继承该父类的子类也会标注该注解
标准注解
@SuppressWarnings关闭特定告警
泛型
< E >常用来声明泛型方法
// 泛型方法 printArray
public static <E> void printArray( E[] inputArray )
{
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
}
< T >常用来声明泛型类
public class Box<T> {
private T t;
public void add(T t) {
this.t = t;
}
public T get() {
return t;
}
}
< ? >类型通配符
不特指某一类,例 如 List<?> 在 逻 辑 上 是List< String >,List< Integer > 等所有 List<具体类型实参>的父类。
<? extends T>表示该通配符所代表的类型是 T 类型的子类。
<? super T>表示该通配符所代表的类型是 T 类型的父类
*类型擦除
编译器编译阶段进行,将泛型用特定的类型代替,一般是Object,因为< T > = < T extends Object>, 如果指定了上限则用上限类,如<? extends String>则用String代替。继承或者实现了某个泛型的类,编译器在类型擦除的时候即生成字节码文件时会增加一个用于类型强转的方法。
复制
直接赋值复制
即复制引用,指向的还是同一个对象,如A a1 = a2;此时a1和a2指向同一个对象。
浅拷贝
只复制值,对象引用的还是同一个对象。
class Resume implements Cloneable{
public Object clone() {
try {
return (Resume)super.clone(); //直接调用Object类的clone方法即可,Object类的clone方法是一个native方法
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
*深拷贝
值类型和对象类型都会重新复制一份,现有对象和原对象无交集。
先将对象序列化再反序列化得到对象也是一种深拷贝
class Student implements Cloneable {
String name;
int age;
Professor p;
Student(String name, int age, Professor p) {
this.name = name;
this.age = age;
this.p = p;
}
public Object clone() {
Student o = null;
try {
o = (Student) super.clone();
} catch (CloneNotSupportedException e) {
System.out.println(e.toString());
}
o.p = (Professor) p.clone();
return o;
}
}
集合
ArrayList、Vector、LinkList
ArrayList底层实现是数组,线程不安全,扩容大小是n+(n>>1),1.5倍,读取某个索引元素快,初始容量为10
Vector底层实现也是数组,线程安全,扩容大小是2n,读取某个索引元素快,初始容量为10
LinkList底层实现是双向链表,线程不安全,添加删除快,读取某个索引元素慢
HashMap和Hashtable
HashMap底层实现是数组+链表+红黑树(双向链表),数组初始容量为16,扩容为2n,链表元素大于等于8个(注意:数组长度小于64时通过扩容来防止链表过长,大于等于64才使用红黑树)变成红黑树,线程不安全,key和value可为空
Hashtable线程安全,但是效率极低,如果需要使用线程安全类建议使用ConcurrentHashMap代替
HashMap底层原理详述
底层数据结构
1.7底层是数组+链表实现
1.8底层是数组+链表+红黑树(红黑树各节点也使用了双向链表:下面有解释为什么使用了红黑树的同时需要加入双向链表)实现
如何解决hash碰撞
解决hash碰撞的方法:
- 重新hash
- 开放地址法(即在hash地址往上或者往下一个存放)
- 公共溢出区(开辟一个新区域专门存放碰撞的值)
- 链地址法
HashMap对于hash碰撞问题,先使用扰动函数降低碰撞概率,若发生碰撞则使用链地址法将hash值相同节点使用链表连接
hash(key)&(n-1)=hash(key)%n(在n是2的幂次方时才成立)
*为什么数组长度必须是2的幂次方?
- 取模运算比位运算慢,2的幂次方可以将取模运算转换成位运算,加快了运算速度
- 在1的条件下,使用位运算公式hash(key)&(n-1)计算值存放位置的时候,如果不是2的幂次方则会使得部分位置永远不会存入数据,浪费空间
- 扩容之后仍是2的幂次方,则n-1之后的二进制数只是在原本长度的二进制数位上高位进位1,即如果原本n-1的二进制数为1111,则扩容之后的二进制数为11111,则与hash(key)进行与运算之后要么还在数组原本的位置(hash(key)进位数为0),要么在原本位置加上扩容前数组长度的位置(hash(key)进位数为1),即用hash(key)&n(n为旧的数组长度)判断是等于0则放在新数组原位置,大于0则放在新数组原位置+n的位置
*扩容机制
1.7超过扩容阈值(0.75*n)则先扩容再插入,头插法
从链表头开始,先将当前节点指向新数组位置,然后移动到新数组位置,依次循环遍历旧数组的链表完成向新数组的转移
1.8先插入,尾插法,再判断是否超过阈值进行扩容
红黑树加入双向链表结构是为了扩容方便,无论是双向链表还是单向链表,扩容的时候都是从链表头开始,先将当前的链表分为原数组位置和原数组位置+n两条链表,如果是单向链表则在分配好之后一次性放入这两个位置,如果是红黑树+双向链表则分配好之后再重新生成红黑树或者变成单向链表(链表长度<=6)再放入
多线程下扩容问题
1.7扩容时的头插法会导致循环引用
1.8扩容时则可能会导致数据丢失
标签:JAVA,JVM,对象,核心,链表,数组,hash,异常 From: https://www.cnblogs.com/jz8912/p/16758652.html