Java
基础
说说自定义注解的场景及实现
利用自定义注解,结合SpringAOP可以完成权限控制、日志记录、统一异常处理、数字签名、数据加解密等功能。
实现场景(API接口数据加解密)
1)自定义一个注解,在需要加解密的方法上添加该注解
2)配置SringAOP环绕通知
3)截获方法入参并进行解密
4)截获方法返回值并进行加密
说一下泛型原理,并举例说明
==泛型就是将类型变成参数传入,使得可以使用的类型多样化,从而实现解耦。===Java 泛型是在 Java1.5 以后出现的,为保持对以前版本的兼容,使用了擦除的方法实现泛型。擦除是指在一定程度无视类型参数 T,直接从 T 所在的类开始向上 T 的父类去擦除,如调用泛型方法, 传入类型参数 T 进入方法内部,若没在声明时做类似 public T methodName(T extends Father t){},Java 就进行了向上类型的擦除,直接把参数 t 当做 Object 类来处理,而不是传进去的 T。即在有泛型的任何类和方法内部,它都无法知道自己的泛型参数,擦除和转型都是在边界上发生,即传进去的参在进入类或方法时被擦除掉,但传出来的时候又被转成了我们设置的 T。在泛型类或方法内,任何涉及到具体类型(即擦除后的类型的子类)操作都不能进行,如 new T(),或者 T.play()(play 为某子类的方法而不是擦除后的类的方法)
说说你对 Java 注解的理解
注解是通过@interface 关键字来进行定义的,形式和接口差不多,只是前面多了一个@
public @interface TestAnnotation {
}
使用时@TestAnnotation 来引用,要使注解能正常工作,还需要使用元注解,它是可以注解到注解上的注解。元标签有@Retention @Documented @Target @Inherited@Repeatable 五种
@Retention 说明注解的存活时间,取值有 RetentionPolicy.SOURCE 注解只在源码阶段保留, 在编译器进行编译时被丢弃;RetentionPolicy.CLASS 注解只保留到编译进行的时候,并不会被加载到 JVM 中。RetentionPolicy.RUNTIME 可以留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。
@Documented 注解中的元素包含到 javadoc 中去
@Target 限 定 注 解 的 应 用 场 景 , ElementType.FIELD 给 属 性 进 行 注解 ; ElementType.LOCAL_VARIABLE 可以给局部变量进行注解;ElementType.METHOD可以给方法进行注解;ElementType.PACKAGE 可以给一个包进行注解 ElementType.TYPE可以给一个类型进行注解,如类、接口、枚举
@Inherited 若一个超类被@Inherited 注解过的注解进行注解,它的子类没有被任何注解应用的话,该子类就可继承超类的注解;
注解的作用:
1)提供信息给编译器:编译器可利用注解来探测错误和警告信息
2)编译阶段:软件工具可以利用注解信息来生成代码、html 文档或做其它相应处理;
3)运行阶段:程序运行时可利用注解提取代码
注解是通过反射获取的,可以通过 Class 对象的 isAnnotationPresent()方法判断它是否应用了某个注解,再通过 getAnnotation()方法获取 Annotation 对象
谈谈你对解析与分派的认识?
解析指方法在运行前,即编译期间就可知的,有一个确定的版本,运行期间也不会改变。解析是静态的,在类加载的解析阶段就可将符号引用转变成直接引用。
分派可分为静态分派和动态分派,重载属于静态分派,覆盖属于动态分派。静态分派是指在 重载时通过参数的静态类型而非实际类型作为判断依据,在编译阶段,编译器可根据参数的 静态类型决定使用哪一个重载版本。动态分派则需要根据实际类型来调用相应的方法。
讲一下常见编码方式?
编码的意义:计算机中存储的最小单元是一个字节即 8bit,所能表示的字符范围是 255个, 而人类要表示的符号太多,无法用一个字节来完全表示,固需要将符号编码,将各种语言翻译成计算机能懂的语言。
1)ASCII 码:总共 128 个,用一个字节的低 7 位表示,0〜31 控制字符如换回车删除等;32~126 是打印字符,可通过键盘输入并显示出来;
2)ISO-8859-1,用来扩展 ASCII 编码,256 个字符,涵盖了大多数西欧语言字符。
3)GB2312:双字节编码,总编码范围是 A1-A7,A1-A9 是符号区,包含 682 个字符,B0-B7 是汉字区,包含 6763 个汉字;
4)GBK 为了扩展 GB2312,加入了更多的汉字,编码范围是 8140~FEFE,有 23940 个码位,能表示 21003 个汉字。
5)UTF-16: ISO 试图想创建一个全新的超语言字典,世界上所有语言都可通过这本字典Unicode 来相互翻译,而 UTF-16 定义了 Unicode 字符在计算机中存取方法,用两个字节来表示 Unicode 转化格式。不论什么字符都可用两字节表示,即 16bit,固叫 UTF-16。
6)UTF-8:UTF-16 统一采用两字节表示一个字符,但有些字符只用一个字节就可表示,浪费存储空间,而 UTF-8 采用一种变长技术,每个编码区域有不同的字码长度。 不同类型的字符可以由1~6个字节组成。
utf-8 编码中的中文占几个字节;int 型几个字节?
utf-8 是一种变长编码技术,utf-8 编码中的中文占用的字节不确定,可能 2 个、3 个、4个, int 型占 4 个字节。
二叉搜索树和平衡二叉树有什么关系,强平衡二叉树(AVL 树)和弱平衡二叉树(红黑树)有什么区别?
二叉搜索树:也称二叉查找树,或二叉排序树。定义也比较简单,要么是一颗空树,要么就是具有如下性质的二叉树:
(1)若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
(2)若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(3)任意节点的左、右子树也分别为二叉查找树;
(4)没有键值相等的节点。
平衡二叉树:在二叉搜索树的基础上多了两个重要的特点
(1)左右两子树的高度差的绝对值不能超过 1(度差可以是1,0,-1);
(2)左右两子树也是一颗平衡二叉树。
红黑书:红黑树是在普通二叉树上,对每个节点添加一个颜色属性形成的,需要同时满足以下五条性质
(1)节点是红色或者是黑色;
(2)根节点是黑色;
(3)每个叶节点(NIL 或空节点)是黑色;
(4)每个红色节点的两个子节点都是黑色的(也就是说不存在两个连续的红色节点);
(5)从任一节点到其没个叶节点的所有路径都包含相同数目的黑色节点。
区别:AVL 树需要保持平衡,但它的旋转太耗时,而红黑树就是一个没有 AVL 树那样平衡,因此插入、删除效率会高于 AVL 树,而 AVL 树的查找效率显然高于红黑树。
树和 B+树的区别,为什么 MySQL 要使用 B+树 B 树?
B树:
(1)关键字集合分布在整颗树中;
(2)任何一个关键字出现且只出现在一个结点中;
(3)搜索有可能在非叶子结点结束;
(4)其搜索性能等价于在关键字全集内做一次二分查找;
B+树:
(1)有 n 棵子树的非叶子结点中含有 n 个关键字(b 树是 n-1 个),这些关键字不保存数据,只用来索引,所有数据都保存在叶子节点(b 树是每个关键字都保存数据);
(2)所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针, 且叶子结点本身依关键字的大小自小而大顺序链接;
(3)所有的非叶子结点可以看成是索引部分,结点中仅含其子树中的最大(或最小) 关键字;
(4)通常在 b+树上有两个头指针,一个指向根结点,一个指向关键字最小的叶子结点;
(5)同一个数字会在不同节点中重复出现,根节点的最大元素就是 b+树的最大元素。
B+树相比于 B 树的查询优势:
(1)B+树的中间节点不保存数据,所以磁盘页能容纳更多节点元素,更“矮胖”;
(2)B+树查询必须查找到叶子节点,B 树只要匹配到即可不用管元素位置,因此 B+树查找更稳定(并不慢);
(3)对于范围查找来说,B+树只需遍历叶子节点链表即可,B 树却需要重复地中序遍历
说说 B-tree 、 B+tree 的区别和使场景?
B-tree:
B-tree 利用了磁盘块的特性进行构建的树。每个磁盘块⼀个节点,每个节点包含了很关键字。把树的节点关键字增多后树的层级比原来的⼆叉树少了,减少数据查找的次数和复杂度。
B-tree 巧妙利用了磁盘预读原理,将⼀个节点的大小设为等于⼀个页(每页为 4K),这样每个节点只需要⼀次 I/O 就可以完全载入。
B-tree 的数据可以存在任何节点中。
B+tree:
B+tree 是 B-tree 的变种,B+tree 数据只存储在叶⼦节点中。这样在 B 树的基础上每个节点存储的关键字数更多,树的层级更少所以查询数据更快,所有指关键字指针都存在叶子节点,所以每次查找的次数都相同所以查询速度更稳定;
数组在内存中如何分配?
静态初始化∶初始化时由程序员显式指定每个数组元素的初始值,由系统决定数组长度,如∶
//只是指定初始值,并没有指定数组的长度,但是系统为自动决定该数组的长度为 4
String[] computers = f"Del1",“Lenovo”,“Apple”,“Acer”};//
//只是指定初始值,并没有指定数组的长度,但是系统为自动决定该数组的长度为 3
String[] names = new String[]{“多啦 A梦”,“大雄”,“静香”}; //
动态初始化∶初始化时由程序员显示的指定数组的长度,由系统为数据每个元素分配初始值,如∶
//只是指定了数组的长度,并没有显示的为数组指定初始值,但是系统会默认给数组数组
元素分配初始值为 nul1
String[] cars = new String[4]; //
静态初始化方式,程序员虽然没有指定数组长度 ,但是系统已经自动帮我们给分配了 ,而动态初始化方式,程序员虽然没有显示的指定初始化值,但是因为 Java 数组是引用类型的变量,所以系统也为每个元素分配了初始化值 nu11,当然不同类型的初始化值也是不一样的,假设是基本类型 int 类型,那么为系统分配的初始化值也是对应的默认值 0。
Cloneable 接口实现原理
Cloneable 接口是 Java 开发中常用的一个接口,它的作用是使一个类的实例能够将自身拷贝到另一个新的实例中,注意,这里所说的"拷贝"拷的是对象实例,而不是类的定义,进一步说,拷贝的是一个类的实例中各字段的值。
在开发过程中,拷贝实例是常见的一种操作,如果一个类中的字段较多,而我们又采用在客户端中逐字段复制的方法进行拷贝操作的话,将不可避免的造成客户端代码繁杂冗长,而且也无法对类中的私有成员进行复制,而如果让需要具备拷贝功能的类实现 cloneable接口,并重写 clone()方法,就可以通过调用 clone()方法的方式简洁地实现实例拷贝功能
深拷贝(深复制)和浅拷贝(浅复制)是两个比较通用的概念,尤其在 C++语言中,若不弄懂,则会在 delete 的时候出问题,但是我们在这幸好用的是 Java。虽然 ava 自动管理对象的回收,但对于深拷贝(深复制)和浅拷贝(浅复制),我们还是要给予足够的重视,因为有时这两个概念往往会给我们带来不小的困惑。
浅拷贝是指拷贝对象时仅仅拷贝对象本身(包括对象中的基本变量),而不拷贝对象包含的引用指向的对象。深拷贝不仅拷贝对象本身,而且拷贝对象包含的引用指向的所有对象。举例来说更加清楚∶对象 A1 中包含对 B1 的引用,B1 中包含对 c1 的引用。浅拷贝A1得到 A2 ,A2 中依然包含对 B1 的引用,B1 中依然包含对 c1 的引用。深拷贝则是对浅拷贝的递归,深拷贝 A1 得到 A2,A2 中包含对 B2( B1 的 copy )的引用,B2 中包含对 C2( C1 的 copy )的引用。若不对 clone()方法进行改写,则调用此方法得到的对象即为浅拷贝
Java 反射机制
Java 反射机制是在运行状态中,对于任意一个类,都能够获得这个类的所有属性和方法,对于任意个对象都能够调用它的任意一个属性和方法。这种在运行时动态的获取信息以及动态调用对象的方法的功能称为 Java 的反射机制。
Class 类与 java.lang.reflect 类库一起对反射的概念进行了支持,该类库包含了 Field,Method,Constructor 类 (每个类都实现了 Member 接口)。这些类型的对象时由 JVM在运行时创建的,用以表示未知类里对应的成员。
这样你就可以使用 Constructor 创建新的对象,用 get()和 set()方法读取和修改与Field 对象关联的字段,用 invoke()方法调用与 Method 对象关联的方法。另外,还可以调用 getFields() getMethods()和 getConstructors()等很便利的方法,以返回表示字段,方法,以及构造器的对象的数组。这样匿名对象的信息就能在运行时被完全确定下来,而在编译时不需要知道任何事情。
import java.lang.reflect.Constructor; public class ReflectTest {
public static void main(String[] args) throws Exception {
Class clazz = null;
clazz = Class.forName("com.jas.reflect.Fruit");
Constructor<Fruit> constructor1 = clazz.getConstructor();
Constructor<Fruit> constructor2 =clazz.getConstructor(String.class);
Fruit fruit1 = constructor1.newInstance();
Fruit fruit2 = constructor2.newInstance("Apple");
}
}
class Fruit{
public Fruit(){
System.out.println("无参构造器 Run
");
}
public Fruit(String type){
System.out.println("有参构造器 Run..........." + type);
}
}
– 运行结果: 无参构造器 Run………… 有参构造器 Run… Apple
Arrays.sort 和 Collections.sort 实现原理和区别
java.uti1.collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。 java.uti1.collections 是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、线程安全等操作。 然后还有混排(Shuffling)、反转(Reverse)、替换所有的元素(fil)、拷贝(copy)、返回 Collections 中最小元素(min)、返回Collections 中最大元素(max)、返回指定源列表中最后一次出现指定目标列表的起始位置( 1astIndexofsubList )、返回指定源列表中第一次出现指定目标列表的起始位置( IndexofSubList )、根据指定的距离循环移动指定列表中的元素(Rotate);事实上 Collections.sort 方法底层就是调用的 array.sort 方法
public static void sort(Object[] a) {
if (LegacyMergeSort.userRequested) legacyMergeSort(a);
else
ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
}
//void java.util.ComparableTimSort.sort()
static void sort(Object[] a, int lo, int hi, Object[] work, int
workBase, int workLen)
{
assert a != null && lo >= 0 && lo <= hi && hi <= a.length; int
nRemaining
= hi - lo;
if (nRemaining < 2)
return;
// Arrays of size 0 and 1 are always sorted
// If array is small, do a "mini-TimSort" with no merges
if (nRemaining < MIN_MERGE) {
int initRunLen = countRunAndMakeAscending(a, lo, hi);
binarySort(a, lo, hi, lo + initRunLen);
return;
}
}
legacyMergeSort (a)∶归并排序 ComparableTimsort.sortO∶
Timsort 排序:Timsort 排序是结合了合并排序(merge sort)和插入排序(insertion sort)而得出的排序
算法 Timsort 的核心过程
TimSort 算法为了减少对升序部分的回溯和对降序部分的性能倒退,将输入按其升序和降序特点进行了分区。排序的输入的单位不是一个个单独的数字,而是一个个的块-分区。其中每一个分区叫一个 run。针对这些 run 序列,每次拿一个 run 出来按规则进行合并。每次合并会将两个 run 合并成一个 run。合并的结果保存到栈中。合并直到消耗掉所有的run,这时将栈上剩余的 run 合并到只剩一个 run 为止。这时这个仅剩的 run 便是排好序的结果。
综上述过程,Timsort 算法的过程包括
(0)如何数组长度小于某个值,直接用二分插入排序算法
(1)找到各个 run,并入栈
(2)按规则合并 run
Java 获取反射的三种方法
1.通过 new 对象实现反射机制
2.通过路径实现反射机制
3.通过类名实现反射机制
public class Student {
private int id; String name;
protected boolean sex;
public float score;
}
public class Get {
//获取反射机制三种方式
public static void main(String[] args) throws ClassNotFoundException
{
//方式一(通过建立对象)
Student stu = new Student(); Class classobj1 = stu.getClass();
System.out.println(classobj1.getName());
//方式二(所在通过路径-相对路径)
Class classobj2 = Class.forName("fanshe.Student");
System.out.println(classobj2.getName());
//方式三(通过类名)
Class classobj3 = Student.class;
System.out.println(classobj3.getName());
}
}
对象的四种引用
强引用只要引用存在,垃圾回收器永远不会回收
Object obj = new Object();.
User user=new User();
可直接通过 obj 取得对应的对象如 obj.eque1s(new objectO);而这样 obj 对象对后面new object 的一个强引用,只有当 obj 这个引用被释放之后,对象才会被释放掉,这也是我们经常所用到的编码形式。
软引用 非必须引用,内存溢出之前进行回收,可以通过以下代码实现
Object obj = new Object();
SoftReference<0bject> sf = new SoftReference(obj);obj= null;
sf.get();//有时候会返回 nul1
这时候 sf 是对 obj 的一个软引用,通过 sf.get()方法可以取到这个对象,当然,当这个对象被标记为需要回收的对象时,则返回 nul;软引用主要用户实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。
弱引用第二次垃圾回收时回收,可以通过如下代码实现
Object obj = new Object();
weakReference<0bject> wf = new weakReference(obj);obj = nu11;
wf.get();//有时候会返回 nu11
wf.isEnQueued ();//返回是否被垃圾回收器标记为即将回收的垃圾
弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,,可以取到,当执行过第二次垃圾回收时,将返回 null。弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的 isEnQueued 方法返回对象是否被垃圾回收器标记。
虚引用 垃圾回收时回收,无法通过引用取到对象值,可以通过如下代码实现
0bject obj = new Object();
PhantomReference<0bject> pf = new
PhantomReference<0bject>(obj);obj=null;
pf.get();//永远返回 nu11
pf.isEnQueuedO);//返回是否从内存中已经删除
虚引用是每次垃圾回收的时候都会被回收,通过虚引用的 get 方法永远获取到的数据为null,因此也被成为幽灵引用。虚引用主要用于检测对象是否已经从内存中删除。
final finally finalize
- final 可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。
- finally 一般作用在 try-catch 代码块中,在处理异常的时候,通常我们将一定要执行的代码方法 finally 代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
- finalize 是一个方法,属于 Obiect 类的一个方法,而 Object 类是所有类的父类,该方法—般由垃圾回收器来调用,当我们调用 System.gc(方法的时候,由垃圾回收器调用finalize(),回收垃圾,—个对象是否可回收的最后判断。
transient 修饰的变量是临时变量吗?
对。
一旦变量被 transient 修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。
transient 关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被 transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现 SERIALIZABLE 接口。被 transient 关键字修饰的变量不再能被序列化,一个静态变量不管是否被 transient 修饰,均不能被序列化。
注意点:在 Java 中,对象的序列化可以通过实现两种接口来实现,若实现的是SERIALIZABLE 接口,则所有的序列化将会自动进行,若实现的是 Externalizable 接口,则没有任何东西可以自动序列化,需要在 writeExternal 方法中进行手工指定所要序列化的变量,这与是否被 transient 修饰无关。
高、中、低三级调度。
高级调度:即作业调度,按照一定策略将选择磁盘上的程序装入内存,并建立进程。(存在与多道批处理系统中)
中级调度:即交换调度,按照一定策略在内外存之间进行数据交换。
低级调度:即 CPU 调度(进程调度),按照一定策略选择就绪进程,占用 cpu 执行。
其中低度调度是必须的。
下面那个查看 80 端口是否被占用?
方式一:ps -ef |grep 80
方式二:netstat -anp |grep :80
方式三:lsof -i:80
方式四:netstat -tunlp |grep :80
方式五:netstat -an |grep :80
索引可以将随机 IO 变成顺序 IO 吗?
对。
随机 IO:假设我们所需要的数据是随机分散在磁盘的不同页的不同扇区中的,那么找到相应的数据需要等到磁臂(寻址作用)旋转到指定的页,然后盘片寻找到对应的扇区,才能找到我们所需要的一块数据,依次进行此过程直到找完所有数据,这个就是随机 IO,读取数据速度较慢。
顺序 IO:假设我们已经找到了第一块数据,并且其他所需的数据就在这一块数据后边,那么就不需要重新寻址,可以依次拿到我们所需的数据,这个就叫顺序 IO。
能在 try{}catch(){}finally{}结构的 finally{}中再次抛出异常吗?
能。在 finally 中抛异常或者 return 会掩盖之前的异常
int a=10 是原子操作吗?
是的。
注意:
i++(或++i)是非原子操作,i++是一个多步操作,而且是可以被中断的。i++可以被分割成3 步,第一步读取 i 的值,第二步计算 i+1;第三部将最终值赋值给 i。
int a = b;不是原子操作。从语法的级别来看,这是也是一条语句,是原子的;但是从实际执行的二进制指令来看,由于现代计算机 CPU 架构体系的限制,数据不可以直接从内存搬运到另外一块内存,必须借助寄存器中断,这条语句一般对应两条
计算机指令,即将变量 b 的值搬运到某个寄存器(如 eax)中,再从该寄存器搬运到变量 a 的内存地址:
mov eax, dword ptr [b]
mov dword ptr [a], eax
既然是两条指令,那么多个线程在执行这两条指令时,某个线程可能会在第一条指令执行完毕后被剥夺 CPU 时间片,切换到另外一个线程而产生不确定的情况。
类与对象的关系?
类是对象的抽象,对象时类的具体,类是对象的模板,对象是类的实例
Super与this表示什么?
Super表示当前类的父类对象
This表示当前类的对象
Collections和Collection有什么区别?
java.utilCollection是一个集合接口(集合类的一个顶级接口),它提供了对集合对象进行基本操作的通用接口方法,Collection接口在java类型中有很多具体的实现,Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接集成接口有List和Set
Collctions则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序,搜索以及线程安全等各种操作
在Queue中poll和remove有什么区别?
相同点:都是返回第一个元素,并在队列中删除返回的对象
不同点:如果没有元素poll会返回null,而remove会直接抛出NoSuchElementException异常
什么是隐式转换,什么是显式转换?
显示转换就是类型强转,把一个大类型的数据强制赋值给小类型的数据,隐式转换就是大范围的变量能够接收小范围的数据,隐式转换和显式转换其实就是自动类型转换和强制类型转换
java中有没有指针?
有指针,但是隐藏了,开发人员无法直接操作指针,由jvm来操作指针
java是值传递还是引用传递?
理论上来说,java都是引用传递,对于基本数据类型,传递是值的副本,而不是值本身,对于对象类型,传递是对象的引用,当在一个方法操作参数的时候,其实操作的是引用所指向的对象
假设吧实例化的数组的变量当成方法参数,当方法执行的时候改变了数组内的元素,那么在方法外,数组元素有发生改变吗?
改变了,因为传递是对象的引用,操作的是引用所指向的对象
throw与throws区别
- throws:用来声明一个方法可能产生的所有异常,不做任何处理而是将异常往上传,谁调用我我就抛给谁
- 用在方法声明后面,跟的是异常类名
- 可以跟多个异常类名,用逗号隔开
- 表示抛出异常,由该方法的调用者来处理
- throws表示出现异常的一种可能性,并不一定会发生这些异常
- throw:则是用来抛出一个具体的异常类型
- 用在方法体内,跟的是异常对象名
- 只能抛出一个异常对象名
- 表示抛出异常,由方法体内的语句处理
- throw则是抛出了异常,执行throw则一定抛出了某种异常
什么是Class文件?Class文件主要的信息结构有哪些?
Class文件是一组以8位字节为基础单位的二进制流,各个数据项严格按顺序排列
Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这样的伪结构仅仅有两种数据类型:无符号数和表
无符号数:是基本数据类型,以U1,U2,U4,U8分别代表一个字节,两个字节,四个字节,八个字节的无符号数,能够用来描写叙述数字,索引引用,数量值或者依照UTF-8编码构成的字符串值
表:由多个无符号数或者其他表作为数据项构成的符合数据类型,全部表习惯性的以_info结尾
形参与实参
形参:全称为:“形式参数”,是在定义方法名和方法体的时候使用的参数,用于接收调用该方法是传入的实际值
实参:全称为"实际参数",是调用该方法时传递给该方法的实际值
用代码演示三种代理
静态代理:
由程序员创建或工具生成代理类的源码,再编译代理类,所谓静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和委托的关系在运行前就确定了
缺点:每个需要代理的对象都需要自己重复编写代理,很不舒服
优点:但是可以面相实际对象或者是接口的方式实现代理
动态代理:
也叫做JDK代理,接口代理,动态代理的对象,是利用JDK的API,动态的在内存中构建代理对象(是根据被代理的接口来动态生成代理类的class文件,并加载运行的过程),这就是动态代理
优点:不用关心代理类,只需要在运行阶段才指定代理哪一个对象
Java与语言特点
- 面向对象(封装,继承,多态)
- 平台无关性( Java 虚拟机实现平台无关性)
- 支持多线程
- ⽀持网络编程并且很方便(Java 语言诞生本身就是为简化⽹络编程设计的,因此 Java 语⾔不仅支持网络编程而且很方便)
- 编译与解释并存
分代收集算法
当前主流VM垃圾收集都采用分代收集(Fenerational Collection)算法,这种算法会根据对象存活周期的不同将内存划分为几块,如JVM中的新生代,老年代,永久代,这样就可以根据个年代特点分别采用最适合的GC算法
Java中的编译器常量是什么?使用它有什么风险?
公共静态不可变(public static final)变量也即是我们所说的编译器常量,这里的public是可选的,实际上这些变量在编译时会被替换掉,因为编译器知道这些变量的值,并且知道这些变量在运行时不能改变,这种存在的一个问题是你使用了一个内部的或第三方库中的共有编译时常量,当时这个值后面被其他人改变了,当时你的客户端仍然在使用老的值,甚至你已经部署了一个jar,为了避免这种情况,当你在更新依赖jar文件时,确保重新编译你的程序
什么是"依赖注入"和"控制反转"?
控制反转(IOC)是Spring框架的核心思想,用我自己的话说,就是你要做一件事,别自己可劲new了,你就说你要干啥,然后外包出去就好
依赖注入(DI)在我浅薄的想法中,就是通过接口的引用和构造方法的表达,将一些事情整好了反过来传给需要用到的地方
面向对象
⾯向对象易维护、易复⽤、易扩展。 因为⾯向对象有封装、继承、多态性的特
性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,⾯向对象性能
⽐⾯向过程低。
String str = “i” 和 String str = new String(“i”)一样吗?
不一样,因为内存的分配方式不一样,String str =“i” 的方式,Java虚拟机会将其分配到常量池中,而String str = new String (“i”)则会被分到堆内存中
如果对象的引用被设置为null,垃圾回收期是否会立即释放对象占用的内存
不会,在下一个垃圾回收周期中,这个对象将是可被回收的
是否了解连接池,使用连接池有什么好处?
数据库连接是非常消耗资源的,影响到程序的性能指标,连接池是用来分配,管理释放数据库连接的,可以使应用重复使用同一个数据库连接,而不是每次都创建一个新的数据库连接连接,通过释放空闲时间较长的数据库连接避免使用数据库因为创建太多的连接而造成的连接遗漏问题,提高了程序性能
你所了解的数据源技术有哪些?使用数据源有什么好处?
Dbcp,c3p0等,用的最多的还是c3p0,因为更加稳定,安全,通过配置文件的形式来维护数据库信息,而不是通过硬编码,当连接的数据库信息发生改变时,不需要再更改程序代码就实现了数据库信息的更新
抽象类能使用final修饰吗?
不能,定义抽象类就是让其他类继承的,如果定义为final该类就不能被基础,这样彼此就会产生矛盾,所以final不能修饰抽象类
Java数据类型
Java中数据类型分两种:
1.基本类型:long,int,byte,float,double,char,boolean
2.对象类型:Long,Integer,Byte,Float,Double其它一切java提供的,或者你自己创建的类。其中Long叫 long的包装类。Integer、Byte和Float也类似,一般包装类的名字首写是数值名的大写开头。
ID用long还是Long?
hibernate、el表达式等都是包装类型,用Long类型可以减少装箱/拆箱;
在hibernate中的自增的hid在实体中的类型要用Long 来定义而不是long。否则在DWR的匹配过程中会出现Marshallingerror:null的错误提示。
到底是选择Long 还是long这个还得看具体环境,如果你认为这个属性不能为null,那么就用long,因为它默认初值为0,如果这个字段可以为null,那么就应该选择Long。
JVM JDK 和 JRE
什么是JVM?java虚拟机包括什么?
JVM:java虚拟机,运用硬件或软件手段实现的虚拟的计算机
Java虚拟机包括:寄存器,堆栈,处理器
JVM
Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。
什么是字节码?采⽤字节码的好处是什么?
JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的⽂件),它不面向任何特定的处理器,只面向虚拟机。由于字节码并不针对⼀种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运⾏。
Java程序从源代码到运行一般步骤:
.java文件(源代码)经过JDK中的javac编译,生成.class文件(JVM中可理解的Java字节),JVM生成机器可执行的二进制机器码
为什么java是编译与解释共存的语言?
.class->机器码 这⼀步。有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT 编译器,而JIT 属于运行时编译。当 JIT 编译器完成第⼀次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语⾔。
Java 既有解释执行,也有编译执行,为了解决解释器的性能瓶颈问题,优化 Java 的性能,引入了即时编译器,大幅度的提高运行效率。
java代码执行过程
JDK 和 JRE
JDK 是 Java Development Kit,它是功能齐全的 Java SDK。
JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟(JVM),Java 类库,java 命令和其他的⼀些基础构件。
Java 和 C++的区别?
- 都是面向对象的语言,都支持封装、继承和多态
- Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多
继承。 - Java 有自动内存管理机制,不需要程序员手动释放无用内存
字符型常量和字符串常量的区别?
- 形式上: 字符常量是单引号引起的⼀个字符; 字符串常量是双引号引起的若干个字符
- 含义上: 字符常量相当于⼀个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表⼀个地
址值(该字符串在内存中存放位置) - 占内存大小字符常量只占 2 个字节; 字符串常量占若干个字节 (注意: char 在 Java 中占两
个字节)
什么是B/S架构?什么是C/S架构?
B/S(Browser/Server),浏览器/服务器程序
C/S(Clent/Server),客户端/服务端,桌面应用程序
Java都有哪些开发平台?
JAVA SE:主要用在客户端开发
JAVA EE:主要用在web应用程序开发
JAVA ME:主要用在嵌入式应用程序开发
四大特性
面向对象思想OOP
抽象
关键词abstract声明的类叫作抽象类,abstract声明的⽅法叫抽象⽅法
⼀个类⾥包含了⼀个或多个抽象⽅法,类就必须指定成抽象类
抽象⽅法属于⼀种特殊⽅法,只含有⼀个声明,没有⽅法体
抽象支付:pay(金额,订单号),默认实现是本地支付,微信支付,支付宝支付,银行卡支付
封装
封装是把过程和数据包围起来,对数据的访问只能通过已定义的接⼝即⽅法
在java中通过关键字private,protected和public实现封装。
封装把对象的所有组成部分组合在⼀起,封装定义程序如何引⽤对象的数据,
封装实际上使⽤⽅法将类的数据隐藏起来,控制⽤户对类的修改和访问数据的程度。 适当的
封装可以让代码更容易理解和维护,也加强了代码的安全性
类封装
⽅法封装
继承
⼦类继承⽗类的特征和行为,使得⼦类对象具有⽗类的方法和属性(包括私有属性和私有⽅法),但是⽗类中的私有属性和⽅法⼦类是⽆法访问,只是拥有。⽗类也叫基类,具有公共的⽅法和属性
动物<-猫
动物<-狗
abstract class AbsPay{
}
WeixinPay extends AbsPay{
}
AliPay extends AbsPay{
}
多态
所谓多态就是指程序中定义的引⽤变量所指向的具体类型和通过该引⽤变量发出的⽅法调⽤在编程时并不确定,⽽是在程序运⾏期间才确定,即⼀个引⽤变量到底会指向哪个类的实例对象,该引⽤变量发出的⽅法调⽤到底是哪个类中实现的⽅法,必须在由程序运⾏期间才能决定。
多态性分为编译时的多态性和运行时的多态性。方法重载实现的是编译时的多态性,而方法重写实现的是运行时的多态性。
优点:减少耦合、灵活可拓展
⼀般是继承类或者重写⽅法实现
Java 中实现多态的机制是什么?
多态是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编译时不确定,在运行期间才确定,一个引用变量到底会指向哪个类的实例。这样就可以不用修改源程序,就可以让引用变量绑定到各种不同的类实现上。
Java 实现多态有三个必要条件: 继承、重定、向上转型,在多态中需要将子类的引用赋值给父类对象,只有这样该引用才能够具备调用父类方法和子类的方法。
构造器 Constructor 是否可被 override?
Constructor 不能被 override(重写),但是可以 overload(重载),所以你可以看到⼀个类中有多个构造函数的情况。
重写和重载的区别
重载Overload:表示同一个类中可以有多个名称相同的方法,但这些方法的参数列表各不相同,参数个数或类型不同
重写Override:重写就是当⼦类继承⾃⽗类的相同⽅法,输⼊数据⼀样,但要做出有别于⽗类的响应时,你就要覆盖⽗类⽅法
重写发⽣在运⾏期,是⼦类对⽗类的允许访问的⽅法的实现过程进⾏重新编写。
- 返回值类型、⽅法名、参数列表必须相同,抛出的异常范围⼩于等于⽗类,访问修饰符范围⼤于等于⽗类。
- 如果⽗类⽅法访问修饰符为 private/final/static 则⼦类就不能重写该⽅法,但是被 static 修饰的⽅法能够被再次声明。
- 构造⽅法⽆法被重写
综上:重写就是⼦类对⽗类⽅法的重新改造,外部样⼦不能改变,内部逻辑可以改变
什么情况下会出现内存溢出,内存泄漏?
内存泄漏的原因很简单:
- 对象是可达的(一直被引用)
- 当时对象不会被使用
常见的内存泄漏的例子:
解决这个内存泄漏问题也很简单,将set设置为null,那就可以避免上述内存泄漏问题了,其他内存内存泄漏得一步一步分析了
内存溢出的原因:
- 内存溢出导致堆栈内存不断增大,从而引发内存溢出
- 大量的jar,class文件加载,装载类的空间不够,溢出
- 操作大量的对象导致对聂村空间已经用满了,溢出
- nio直接操作内存,内存过大导致溢出
解决:
查看程序是否存在内存泄漏的问题
设置参数加大空间
代码中是否存在死循环或者循环产生过多重复的对象实
查看是否使用nio直接操作内存
String,StringBuffer 和 StringBuilder 的区别是什么?
String为什么是不可变的?
可变性
简单的来说:String 类中使用 final 关键字修饰字符数组来保存字符串, private final char value[] (在 Java 9 之后,String 类的实现改⽤ byte 数组存储字符串 private final byte[] value),所以 String 对象是不可变的。
⽽ StringBuilder 与 StringBuffer 都继承⾃ AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使⽤字符数组保存字符串 char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的。
线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是StringBuilder 与 StringBuffer 的公共⽗类,定义了⼀些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共⽅法。StringBuffer 对⽅法加了同步锁或者对调⽤的⽅法加了同步锁,所以是线程安全的。StringBuilder 并没有对⽅法进⾏加同步锁,所以是⾮线程安全的。
以StringBuffer的apend举例:
性能
每次对 String 类型进⾏改变的时候,都会⽣成⼀个新的 String 对象,然后将指针指向新的 String对象。StringBuffer 每次都会对 StringBuffer 对象本身进⾏操作,⽽不是⽣成新的对象并改变对象引⽤。
相同情况下使⽤ StringBuilder 相⽐使⽤ StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的⻛险
总结:
- 操作少量的数据: 适⽤ String
- 单线程操作字符串缓冲区下操作⼤量数据: 适⽤ StringBuilder
- 多线程操作字符串缓冲区下操作⼤量数据: 适⽤ StringBuffer
String 为什么要设计成不可变的?
1)字符串常量池需要 String 不可变。因为 String 设计成不可变,当创建一个 String 对象时, 若此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。如果字符串变量允许改变,会导致各种逻辑错误,如改变一个对象会影响到另一个独立对象。
2)String 对象可以缓存 hashCode。字符串的不可变性保证了 hash 码的唯一性,因此可以缓存 String 的 hashCode,这样不用每次去重新计算哈希码。在进行字符串比较时,可以直接比较 hashCode,提高了比较性能;
3)安全性。String 被许多 java 类用来当作参数,如 url 地址,文件 path 路径,反射机制所需的 Strign 参数等,若 String 可变,将会引起各种安全隐患。
自动装箱与拆箱
装箱:将基本类型⽤它们对应的引⽤类型包装起来;
拆箱:将包装类型转换为基本数据类型;
在一个静态方法内调用⼀个非静态成员为什么是非法的?
由于静态⽅法可以不通过对象进⾏调⽤,因此在静态⽅法⾥,不能调用其他非静态变量,也不可以访问⾮静态变量成员。
Non-static field ‘a’ cannot be referenced from a static context
无参构造
Java 程序在执⾏⼦类的构造⽅法之前,如果没有用 super() 来调用父类特定的构造方法,则会调用父类中“没有参数的构造⽅法”。因此,如果⽗类中只定义了有参数的构造⽅法,⽽在⼦类的构造⽅法中⼜没有⽤ super() 来调⽤⽗类中特定的构造⽅法,则编译时将发⽣错误,因为 Java 程序在⽗类中找不到没有参数的构造⽅法可供执⾏。解决办法是在⽗类⾥加上⼀个不做事且没有参数的构造⽅法。
接口
接口是否可以继承接口?接口是否支持多继承?类是否支持多继承?接口里面是否可以有方法实现?
接⼝⾥可以有静态⽅法和⽅法体
接⼝中所有的⽅法必须是抽象⽅法(JDK8之后就不是)
接⼝不是被类继承了,而是要被类实现
接⼝⽀持多继承, 类不⽀持多个类继承
⼀个类只能继承⼀个类,但是能实现多个接⼝,接⼝能继承另⼀个接⼝,接⼝的继承使⽤extends关键字,和类继承⼀样
JDK8接口新特性
interface中可以有static方法,但必须有⽅法实现体,该⽅法只属于该接⼝,接⼝名直接调⽤该⽅法
接⼝中新增default关键字修饰的方法,default⽅法只能定义在接⼝中,可以在⼦类或⼦接⼝ 中被重写default定义的⽅法必须有⽅法体
⽗接⼝的default⽅法如果在⼦接⼝或⼦类被重写,那么⼦接⼝实现对象、⼦类对象,调⽤该方法,以重写为准
本类、接⼝如果没有重写⽗类(即接⼝)的default⽅法,则在调⽤default⽅法时,使⽤⽗类(接口) 定义的default⽅法逻辑
接口和抽象类
- 接⼝的⽅法默认是 public ,所有⽅法在接⼝中不能有实现,即只能有抽象方法(Java 8 开始接⼝⽅法可以有默认实现),而抽象类可以有非抽象的⽅法。
- 接⼝中除了 static 、 final 变量,不能有其他变量,⽽抽象类中则不⼀定。
- ==⼀个类可以实现多个接⼝,但只能实现⼀个抽象类。==接口自己本身可以通过 extends 关键字扩展多个接⼝。
- 接⼝⽅法默认修饰符是 public ,抽象⽅法可以有 public 、 protected 和 default 这些修饰符(抽象⽅法就是为了被重写所以不能使⽤ private 关键字修饰!)。
- 从设计层⾯来说,抽象是对类的抽象,是⼀种模板设计,而接口是对行为的抽象,是⼀种行为的规范。
成员变量与局部变量的区别有哪些?
- 从语法形式上看:成员变量是属于类的,而局部变量是在⽅法中定义的变量或是⽅法的参数;成员变量可以被 public , private , static 等修饰符所修饰,⽽局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
- 从变量在内存中的存储⽅式来看:如果成员变量是使⽤ static 修饰的,那么这个成员变量是属于类的,如果没有使⽤ static 修饰,这个成员变量是属于实例的。对象存于堆内存,如果局部变量类型为基本数据类型,那么存储在栈内存,如果为引⽤数据类型,那存放的是指向堆内存对象的引⽤或者是指向常量池中的地址。
- 从变量在内存中的⽣存时间上看:成员变量是对象的⼀部分,它随着对象的创建⽽存在,⽽局部变量随着⽅法的调用而⾃动消失。
- 成员变量如果没有被赋初值:则会⾃动以类型的默认值⽽赋值(⼀种情况例外:被 final 修饰的成员变量也必须显式地赋值),⽽局部变量则不会⾃动赋值。
构造方法
⼀个类的构造⽅法的作⽤是什么? 若⼀个类没有声明构造⽅法,该程序能正确执⾏吗? 为什么?
主要作用是完成对类对象的初始化⼯作。可以执⾏。因为⼀个类即使没有声明构造⽅法也会有默认的不带参数的构造⽅法。
构造⽅法有哪些特性?
- 名字与类名相同。
- 没有返回值,但不能⽤ void 声明构造函数。
- ⽣成类的对象时⾃动执⾏,⽆需调⽤。
静态方法和实例方法有何不同
- 在外部调⽤静态⽅法时,可以使⽤"类名.⽅法名"的⽅式,也可以使⽤"对象名.⽅法名"的⽅式。⽽实例⽅法只有后⾯这种⽅式。也就是说,调⽤静态⽅法可以⽆需创建对象。
- 静态⽅法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态⽅法),⽽不允许访问实例成员变量和实例⽅法;实例⽅法则⽆此限制。
== 与 equals(重要)
两个等号,如果是基本数据类型判断的是值,引用数据类型判断的是内存地址
equals() : 它的作⽤也是判断两个对象是否相等。但它⼀般有两种使⽤情况:
情况 1:类没有覆盖 equals() ⽅法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
情况 2:类覆盖了 equals() ⽅法。⼀般,我们都覆盖 equals() ⽅法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
- String 中的 equals ⽅法是被重写过的,因为 object 的 equals ⽅法是比较的对象的内存地址,而String 的 equals ⽅法比较的是对象的值。
- 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引⽤。如果没有就在常量池中重新创建⼀个 String 对象
hashCode 与 equals (重要)
hashCode
==hashCode() 的作⽤是获取哈希码,也称为散列码;它实际上是返回⼀个 int 整数。==这个哈希码的作⽤是确定该对象在哈希表中的索引位置。 hashCode() 定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。另外需要注意的是: Object 的 hashcode ⽅法是本地⽅法,也就是⽤ c 语⾔或 c++ 实现的,该⽅法通常⽤来将对象的 内存地址 转换为整数之后返回。
public native int hashCode();
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就
利⽤到了散列码!(可以快速找到所需要的对象)
为什么重写 equals 时必须重写 hashCode ⽅法?
如果两个对象相等,则 hashcode ⼀定也是相同的。两个对象相等,对两个对象分别调⽤ equals⽅法都返回 true。但是,两个对象有相同的 hashcode 值,它们也不⼀定是相等的 。因此,equals ⽅法被覆盖过,则 hashCode ⽅法也必须被覆盖。
final关键字
主要⽤在三个地⽅:变量、⽅法、类。
变量:如果是基本数据类型,那么加上final字段后,其值就不能进行更改,如果是引用数据类型,那么就不能让其指向另一个对象
方法:1.锁定方法,防止继承类修改它的含义;2.是效率。在早期的 Java 实现版本中,会将 final ⽅法转为内嵌调⽤。但是如果⽅法过于庞大,可能看不到内嵌调⽤带来的任何性能提升(现在的 Java 版本已经不需要使⽤final ⽅法进⾏这些优化了)。类中所有的 private ⽅法都隐式地指定为 final。
类:加了final字段的类不允许被继承,其中所有成员方法被隐式地在指定为final方法
异常
Java异常类结构层次图
在 Java 中,所有的异常都有⼀个共同的祖先 java.lang 包中的 Throwable 类。 Throwable 类有两个重要的子类** Exception (异常)**和 Error (错误)。Exception 能被程序本身处理( try catch ),Error 是⽆法处理的(只能尽量避免)。
异常处理总结
- try 块: ⽤于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟⼀个 finally 块。
- catch 块: ⽤于处理 try 捕获到的异常。
- finally 块: ⽆论是否捕获或处理异常, finally 块⾥的语句都会被执⾏。当在 try 块或catch 块中遇到 return 语句时, finally 语句块将在⽅法返回之前被执⾏。
在以下 3 种特殊情况下, finally 块不会被执⾏:
- 在 try 或 finally 块中⽤了 System.exit(int) 退出程序。但是,如果 System.exit(int) 在异常语句之后, finally 还是会被执⾏
- 程序所在的线程死亡。
- 关闭 CPU。
Java序列化
Java 序列化中如果有些字段不想进⾏序列化,怎么办?
使用transient或者transient注解
transient 关键字的作⽤是:阻⽌实例中那些⽤此关键字修饰的的变量序列化;当对象被反序列化时,被transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和⽅法。
键盘输入
⽅法 1:通过 Scanner
Scanner sc = new Scanner(System.in);
方法2:通过BufferedReader
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
JAVA注解
Annotation(注解)是 Java 提供的一种对元程序中元素关联信息和元数据(metadata)的途径和方法。Annatation(注解)是一个接口,程序可以通过反射来获取指定程序中元素的 Annotation对象,然后通过该 Annotation 对象来获取注解中的元数据信息。
四种标准元注解
@Target
@Target说明了Annotation所修饰的对象范围: Annotation可被用于packages、types(类、接口、枚举、Annotation 类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch 参数。在 Annotation 类型的声明中使用了 target 可更加明晰其修饰的目标
@Retention 定义 被保留的时间长短
Retention 定义了该 Annotation 被保留的时间长短:表示需要在什么级别保存注解信息,用于描
述注解的生命周期(即:被描述的注解在什么范围内有效),取值(RetentionPoicy)由:
SOURCE:在源文件中有效(即源文件保留)
CLASS:在 class 文件中有效(即 class 保留)
RUNTIME:在运行时有效(即运行时保留)
@Documented 描述-javadoc
@ Documented 用于描述其它类型的 annotation 应该被作为被标注的程序成员的公共 API,因此可以被例如 javadoc 此类的工具文档化。
@Inherited 阐述了某个被标注的类型是被继承的
@Inherited 元注解是一个标记注解,@Inherited 阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited 修饰的 annotation 类型被用于一个 class,则这个annotation 将被用于该class 的子类。
注解处理器
/1:*** 定义注解*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitProvider {
/**供应商编号*/
public int id() default -1;
/*** 供应商名称*/
public String name() default "";
/** * 供应商地址*/
public String address() default "";
}
//2:注解使用
public class Apple {
@FruitProvider(id = 1, name = "陕西红富士集团", address = "陕西省西安市延安路")
private String appleProvider;
public void setAppleProvider(String appleProvider) {
this.appleProvider = appleProvider;
}
public String getAppleProvider() {
return appleProvider;
} }/3:*********** 注解处理器 ***************/
public class FruitInfoUtil {
public static void getFruitInfo(Class<?> clazz) {
String strFruitProvicer = "供应商信息:";
Field[] fields = clazz.getDeclaredFields();//通过反射获取处理注解
for (Field field : fields) {
if (field.isAnnotationPresent(FruitProvider.class)) {
FruitProvider fruitProvider = (FruitProvider) field.getAnnotation(FruitProvider.class);
//注解信息的处理地方
strFruitProvicer = " 供应商编号:" + fruitProvider.id() + " 供应商名称:"
+ fruitProvider.name() + " 供应商地址:"+ fruitProvider.address();
System.out.println(strFruitProvicer);
}
}
} }
public class FruitRun {
public static void main(String[] args) {
FruitInfoUtil.getFruitInfo(Apple.class);
/***********输出结果***************/
// 供应商编号:1 供应商名称:陕西红富士集团 供应商地址:陕西省西安市延
} }
反射
反射机制是什么
反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
Person p=new Student();
其中编译时类型为 Person,运行时类型为 Student。
反射能做什么
- 在运行时判断任意一个对象所属的类;
- 在运行时构造任意一个类的对象;
- 在运行时判断任意一个类所具有的成员变量和方法;
- 在运行时调用任意一个对象的方法;
- 生成动态代理
I/O
分类
- 按照流的流向划分:输入流和输出流
- 按照操作单元划分:字节流和字符流
- 按照留的角色划分:节点流和处理流
既然有了字节流,为什么还要有字符流?
不管是⽂件读写还是⽹络发送接收,信息的最⼩存储单元都是字节,那为什么I/O 流操作要分为字节流操作和字符流操作呢?
字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就⼲脆提供了⼀个直接操作字符的接⼝,⽅便我们平时对字符进⾏流操作。如果⾳频⽂件、图⽚等媒体⽂件⽤字节流⽐较好,如果涉及到字符的话使⽤字符流⽐好。
BIO,NIO,AIO 有什么区别?
BIO (Blocking I/O): 同步阻塞 I/O 模式
NIO (Non-blocking/New I/O): NIO 是⼀种同步⾮阻塞的 I/O 模型
AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引⼊了 NIO 的改进版 NIO 2,它是异步⾮阻塞的 IO 模型。
深拷贝 vs 浅拷贝
浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝
深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容
异常
如果某个方法不能按照正常的途径完成任务,就可以通过另一种路径退出方法。在这种情况下会抛出一个封装了错误信息的对象。此时,这个方法会立刻退出同时不返回任何值。另外,调用这个方法的其他代码也无法继续执行,异常处理机制会将代码执行交给异常处理器。
异常分类
**Throwable **是 Java 语言中所有错误或异常的超类。下一层分为 **Error 和 Exception **
Error
- Error 类是指 java 运行时系统的内部错误和资源耗尽错误。应用程序不会抛出该类对象。如果出现了这样的错误,除了告知用户,剩下的就是尽力使程序安全的终止。
Exception(RuntimeException、CheckedException)
2. Exception 又有两个分支,一个是运行时异常 RuntimeException ,一个是CheckedException。
RuntimeException 如 : NullPointerException 、 ClassCastException ;一个是检查异常CheckedException,如 I/O 错误导致的 IOException、SQLException。 RuntimeException 是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。
JAVA内部类
Java 类中不仅可以定义变量和方法,还可以定义类,这样定义在类内部的类就被称为内部类。根据定义的方式不同,内部类分为静态内部类,成员内部类,局部内部类,匿名内部类四种。
静态内部类:public static class Inner
成员内部类:public class Inner
局部内部类:定义在方法中的类,就是局部类。如果一个类只在某个方法中使用,则可以考虑使用局部类。
public void test(final int c) {
final int d = 1;
class Inner {
public void print() {
System.out.println(c);
}
}
}
匿名内部类:
test.test(new Bird() {
public int fly() {
return 10000;
}
public String getName() {
return "大雁";
}
});
JDBC加载驱动
- 加载数据库驱动类
- 打开数据库链接
- 执行sql语句
- 处理返回结果
- 关闭资源
在使用jdbc的时候,如何防止出现sql注入
使用CallableStatement
怎么在JDBC内调用一个存储过程
使用PreparedStatement类,而不是使用Statement类
JAVA序列化
保存(持久化)对象及其状态到内存或者磁盘
Java 平台允许我们在内存中创建可复用的 Java 对象,但一般情况下,只有当 JVM 处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比 JVM 的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。
Java 对象序列化就能够帮助我们实现该功能。
序列化对象以字节数组保持-静态成员不保存
使用 Java 对象序列化,==在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。==必须注意地是,==对象序列化保存的是对象的”状态”,即它的成员变量。==由此可知,对象序列化不会关注类中的静态变量。
序列化用户远程对象传输
==除了在持久化对象时会用到对象序列化之外,当使用 RMI(远程方法调用),或在网络中传递对象时,==都会用到对象序列化。Java序列化API为处理对象序列化提供了一个标准机制,该API简单易用。
Serializable 实现序列化
在 Java 中,只要一个类实现了 java.io.Serializable 接口,那么它就可以被序列化。
ObjectOutputStream 和 ObjectInputStream 对对象进行序列化及反序列化
通过 ObjectOutputStream 和 ObjectInputStream 对对象进行序列化及反序列化。writeObject 和 readObject 自定义序列化策略
在类中增加 writeObject 和 readObject 方法可以实现自定义序列化策略。
序列化 ID
虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID)
Transient 关键字阻止该变量被序列化到文件中
- 在变量声明前加上 Transient 关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
- 服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。
集合
LinkedHashMap 的应用
基于 LinkedHashMap 的访问顺序的特点,可构造一个 LRU(Least Recently Used)最近最少使用简单缓存。也有一些开源的缓存产品如 ehcache 的淘汰策略( LRU )就是在LinkedHashMap 上扩展的。
HashMap 是线程安全的吗,为什么不是线程安全的?
不是线程安全的;
如果有两个线程 A 和 B,都进行插入数据,刚好这两条不同的数据经过哈希计算后得到的哈希码是一样的,且该位置还有其他的数据。假设一种情况,线程 A 通讨 if 判断 ,该位置没有哈希冲突,进入了 if 语句,还没有进行数据插入,这时候 CPU 就把资源让给了线程 B,线程 A 停在了 if 语句里面,线程 B 判断该位置没有哈希冲突(线程 A 的数据还没插入),也进入了 if 语句 ,线程 B 执行完后,轮到线程 A 执行,现在线程 A 直接在该位置插入而不用再判断。这时候,你会发现线程 A 把线程 B 插入的数据给覆盖了。发生了线程不安全情况。本来在 HashMap 中,发生哈希冲突是可以用链表法或者红黑树来解决的,但是在多线程中,可能就直接给覆盖了。
HashMap 如何解决 Hash 冲突?
通过引入单向链表来解决 Hash 冲突。当出现 Hash 冲突时,比较新老 key 值是否相等,如果相等,新值覆盖旧值。如果不相等,新值会存入新的 Node 结点,指向老节点,形成链式结构,即链表。当 Hash 冲突发生频繁的时候,会导致链表长度过长,以致检索效率低,所以 JDK1.8 之后引入了红黑树,当链表长度大于 8 时,链表会转换成红黑书,以此提高查询性能。
HashSet 是如何保证不重复的?
向 HashSet 中 add ()元素时,判断元素是否存在的依据,不仅要比较 hash 值,同时还要结合 equles 方法比较。
HashSet 中的 add ()方法会使用 HashMap 的 add ()方法。以下是 HashSet 部分源码:
private static final Object PRESENT = new Object(); private transient
HashMap<E,Object> map;
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
HashMap 的 key 是唯一的,由上面的代码可以看出 HashSet 添加进去的值就是作为
HashMap 的 key。所以不会 重复(HashMa 比较 key 是否相等是先比较 hashcode 在比
较 equals)。
List 和 Set 的区别
List , Set 都是继承自 Collection 接口
List 特点:元素有放入顺序,元素可重复 ,
Set 特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉,(元素虽然无放入顺序,但是元素在 set 中的位置是有该元素的 HashCode 决定的,其位置其实是固定的,加入 Set 的 Object 必须定义 equals ()方法,另外 list 支持 for 循环,也就是通过下标来遍历,也可以用迭代器,但是 set 只能用迭代,因为他无序,无法用下标来取得想要的值。)
Set 和 List 对比
Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。List:和数组类似,List 可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变。
CopyOnWriteArrayList 是线程安全的吗?
是的
CopyOnWriteArrayList 使用了一种叫写时复制的方法,当有新元素添加到 CopyOnWriteArrayList 时,先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,再将原来的数组引用指向到新数组。创建新数组,并往新数组中加入一个新元素,这个时候,array 这个引用仍然是指向原数组的。当元素在新数组添加成功后,将 array这个引用指向新数组。
CopyOnWriteArrayList 的整个 add 操作都是在锁的保护下进行的。这样做是为了避免在多线程并发 add 的时候,复制出多个副本出来,把数据搞乱了,导致最终的数组数据不是我们期望的。
public boolean add(E e) {
//1、先加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//2、拷贝数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
//3、将元素加入到新数组中
newElements[len] = e;
//4、将 array引用指向到新数组
setArray(newElements);
return true;
} finally {
//5、解锁
lock.unlock();
}
}
由于所有的写操作都是在新数组进行的,这个时候如果有线程并发的写,则通过锁来控制,如果有线程并发的读,则分几种情况:
- 如果写操作未完成,那么直接读取原数组的数据;
- 如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据;
- 如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。
可见,CopyOnWriteArrayList 的读操作是可以不用加锁的。
CopyOnWriteArrayList 有几个缺点:
- 由于写操作的时候,需要拷贝数组,会消耗内存,
- 如果原数组的内容比较多的情况下,可能导致 young gc 或者 full gc
- 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的,
虽然 CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;CopyOnWriteArrayList 合适读多写少的场景,不过这类慎用因为谁也没法保证 CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次 add/set 都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。
CopyOnWriteArrayList 透露的思想:读写分离,读和写分开 最终一致性 使用另外开辟空间的思路,来解决并发冲突
数组越界问题
一般来讲我们使用时,会用一个线程向容器中添加元素,一个线程来读取元素,而读取的操作往往更加频繁。写操作加锁保证了线程安全,读写分离保证了读操作的效率,简直完美。如果这时候有第三个线程进行删除元素操作,读线程去读取容器中最后一个元素,读之前的时候容器大小为 i,当去读的时候删除线程突然删除了一个元素,这个时候容器大小变为了 i-1,读线程仍然去读取第 i 个元素,这时候就会发生数组越界。
测试一下,首先向 CopyOnWriteArrayList 里面塞 10000 个测试数据,启动两个线程,一个不断的删除元素,一个不断的读取容器中最后一个数据。
public void test(){
for(int i = 0; i<10000; i++){
list.add("string" + i);
}
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
if (list.size() > 0) {
String content = list.get(list.size() - 1);
}else {
break;
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
if(list.size() <= 0){
break;
}
list.remove(0);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
Array 和 ArrayList 有何区别?
Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。
Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。
Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有。
Collection 和 Collections 有什么区别?
Collection 是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的子类,比如 List、Set 等。
Collections 是一个包装类,包含了很多静态方法,不能被实例化,就像一个工具类,比如提供的排序方法: Collections. sort(list)。
迭代器Iterator是什么?
地带起是一种设计模式,它是一个兑现,它可以遍历并选择序列中的对象,而开发人员不需要了解该序列的底层机构,迭代器通常被称为"轻量级"对象,因为创建它的代价小
怎么确保一个集合不能被修改?
可以使用Collections.unmodifiableCollection(Collection c)方法来创建一个只读集合,这样改变集合的任何操作都会抛出Java.langUnsupportedoperationException异常
List
Java 的 List 是非常常用的数据类型。List 是有序的 Collection。Java List 一共三个实现类:分别是 ArrayList、Vector 和 LinkedList。
ArrayList(数组)
ArrayList 是最常用的 List 实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数组的数据复制到新的存储空间中。当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。
Vector(数组,线程同步)
Vector 与 ArrayList 一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写 Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问 ArrayList 慢。
LinkList(链表)
LinkedList 是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,他还提供了 List 接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。
Set
Set 注重独一无二的性质,该体系集合用于存储无序(存入和取出的顺序不一定相同)元素,值不能重复。对象的相等性本质是对象 hashCode 值(java 是依据对象的内存地址计算出的此序号)判断的,如果想要让两个不同的对象视为相等的,就必须覆盖 Object 的 hashCode 方法和 equals 方法。
说一下HashSet的实现原理?
HashSet底层由HashMap实现
HashSet的值存放于HashMap的key上
HashMap的value统一为PRESENT
HashSet(Hash表)
哈希表边存放的是哈希值。HashSet 存储元素的顺序并不是按照存入时的顺序(和 List 显然不同) 而是按照哈希值来存的所以取数据也是按照哈希值取得。元素的哈希值是通过元素的hashcode 方法来获取的, HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较equals 方法 如果 equls 结果为 true ,HashSet 就视为同一个元素。如果 equals 为 false 就不是同一个元素。哈希值相同 equals 为 false 的元素是怎么存储呢,就是在同样的哈希值下顺延(可以认为哈希值相同的元素放在一个哈希桶中)。也就是哈希一样的存一列。
说说List,Set,Map三者的区别?
List (对付顺序的好帮⼿): 存储的元素是有序的、可重复的。
Set (注重独⼀⽆⼆的性质): 存储的元素是⽆序的、不可重复的。
**Map **(⽤ Key 来搜索的专家): 使⽤键值对(kye-value)存储,类似于数学上的函数y=f(x),“x”代表 key,"y"代表 value,Key 是⽆序的、不可重复的,value 是⽆序的、可重复的,每个键最多映射到⼀个值。
RandomAccess 接⼝
public interface RandomAccess {
}
RandomAccess 接⼝中什么都没有定义。所以,RandomAccess 接⼝不过是⼀个标识罢了。标识什么? 标识实现这个接⼝的类具有随机访问功能。在 binarySearch ⽅法中,它要判断传⼊的 list 是否RamdomAccess 的实例,如果是,调用 indexedBinarySearch() ⽅法,如果不是,那么调⽤ iteratorBinarySearch() ⽅法
比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
HashSet 是 Set 接⼝的主要实现类 , HashSet 的底层是 HashMap ,线程不安全的,可以存储 null 值;
LinkedHashSet 是 HashSet 的⼦类,能够按照添加的顺序遍历;
TreeSet 底层使⽤红⿊树,能够按照添加元素的顺序进⾏遍历,排序的⽅式有⾃然排序和定制排
序。
List
什么是 Fail-Fast、什么是 Fail-Safe?
Fail-Fast:一旦发现遍历的同时其他人来修改,则立刻抛出异常
Fail-Safe:发现遍历的同事其他人来修改,应当能有应对策略,例如牺牲一致性来让整个遍历运行完成
- ArrayList 是 fail-fast 的典型代表,遍历的同时不能修改,尽快失败
- CopyOnWriteArrayList 是 fail-safe 的典型代表,遍历的同时可以修改,原理是读写分离
Vector和ArrayList、LinkedList联系和区别
从线程安全角度:
ArrayList:底层是数组实现,线程不安全,查询和修改非常快根,根据下标就可以进行操作时间复杂度1,但是增加和删除慢,需要移动大量的元素,时间复杂度n
LinkedList: 底层是双向链表,线程不安全,查询和修改速度慢,需要进行遍历操作,时间复杂度为n,但是增加和删除速度快,时间复杂度1
Vector: 底层是数组(Object[] )实现,线程安全的,操作的时候使用synchronized进行加锁
使用场景:
Vector已经很少用了
增加和删除场景多则用LinkedList
查询和修改多则用ArrayList
如果需要保证线程安全,ArrayList应该怎么做,用有几种方式
方式一:自己写个包装类,根据业务一般是add/update/remove加锁
方式二:Collections.synchronizedList(new ArrayList<>()); 使用synchronized加锁
//本质还是加锁
List list2 = Collections.synchronizedList(list1);
方式三:CopyOnWriteArrayList<>() 使用ReentrantLock加锁
CopyOnWriteArrayList和Collections.synchronizedList实现线程安全有什么区别
CopyOnWriteArrayList:执行修改操作时,会拷贝一份新的数组进行操作(add、set、remove等),代价十分昂贵,在执行完修改后将原来集合指向新的集合来完成修改操作,源码里面用ReentrantLock可重入锁来保证不会有多个线程同时拷贝一份数组
以添加元素源码举例:
public boolean add(E e) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
es = Arrays.copyOf(es, len + 1);
es[len] = e;
setArray(es);
return true;
}
}
场景:读高性能,适用读操作远远大于写操作的场景中使用(读的时候是不需要加锁的,直接获取,删除和增加是需要加锁的, 读多写少)
Collections.synchronizedList:线程安全的原因是因为它几乎在每个方法中都使用了synchronized同步锁
场景:CopyOnWriteArrayList适合读多的场景,synchronizedList适合写多的场景
场景:写操作性能比CopyOnWriteArrayList好,读操作性能并不如CopyOnWriteArrayList
CopyOnWriteArrayList的设计思想是怎样的,有什么缺点?
设计思想:读写分离+最终一致
缺点:内存占用问题,写时复制机制,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象,如果对象过大(大对象会直接保存在老生代)则容易发生Yong GC和Full GC
ArrayList的扩容机制
注意:JDK1.7之前ArrayList默认大小是10,JDk1.8开始是未指定集合容量,默认是0,若已经指定的大小,(小于集合大小,小于10),当集合第一次添加元素的时候,集合大小扩容为10
ArrayList的元素个数大于其容量,扩容的大小=原始大小+原始大小/2
tips:关于ArraysList.addAll扩容机制详解
addAll(Collection c) 没有元素时,扩容为 Math.max(10, 实际元素个数),有元素时为 Math.max(原容量 1.5 倍, 实际元素个数)
if(list.size=0&&addAll.size>10) then list.size = addAll.size else if ((addAll+list.size)> 下次扩容容量) then list.size扩容 = addAll+list.size else if((addAll+list.size)< 下次扩容容量) then list.size扩容 = 下次扩容的容量
Map
了解Map吗?用过哪些Map的实现
HashMap、Hashtable、LinkedHashMap、TreeMap、ConcurrentHashMap
HashMap:底层是基于数组+链表,JDK8以后引入了红黑树,当链表大于8的时候,则会转成红黑树,非线程安全的,默认容量是16、允许有空的健和值
Hashtable:基于哈希表实现,线程安全的(加了synchronized),默认容量是11,不允许有null的健和值
HashMap 和 Hashtable 的区别
- HashMap 是⾮线程安全的, HashTable 是线程安全的,因为 HashTable 内部的⽅法基本都经过 synchronized 修饰。
- 效率: 因为线程安全的问题, HashMap 要⽐ HashTable 效率⾼⼀点。
- 对 Null key 和 Null value 的⽀持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有⼀个,null 作为值可以有多个;HashTable 不允许有 null 键和 null 值,否则会抛出NullPointerException 。
- 初始容量⼤⼩和每次扩充容量⼤⼩的不同 :创建时如果不指定容量初始值, Hashtable默认的初始⼤⼩为 11,之后每次扩充,容量变为原来的 2n+1。 HashMap 默认的初始化⼤⼩为 16。之后每次扩充,容量变为原来的 2 倍。
hashCode()和equals()
hashcode
顶级类Object里面的方法,所有的类都是继承Object,返回是一个int类型的数
根据一定的hash规则(存储地址,字段,长度等),映射成一个数组,即散列值
equals
顶级类Object里面的方法,所有的类都是继承Object,返回是一个boolean类型
根据自定义的匹配规则,用于匹配两个对象是否一样,一般逻辑如下
//判断地址是否一样
//非空判断和Class类型判断
//强转
//对象里面的字段一一匹配
使用场景:对象比较、或者集合容器里面排重、比较、排序
hashCode() 与 equals() 的相关规定:
- 如果两个对象相等,则 hashcode ⼀定也是相同的
- 两个对象相等,对两个 equals() ⽅法返回 true
- 两个对象有相同的 hashcode 值,它们也不⼀定是相等的
- 综上, equals() ⽅法被覆盖过,则 hashCode() ⽅法也必须被覆盖
- hashCode() 的默认⾏为是对堆上的对象产⽣独特值。如果没有重写 hashCode() ,则该class 的两个对象⽆论如何都不会相等(即使这两个对象指向相同的数据)。
手写Hashcode和equals
public class User {
private int age;
private String name;
private Date time;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getTime() {
return time;
}
public void setTime(Date time) {
this.time = time;
}
@Override
public int hashCode() {
//int code = age/name.length()+time.hashCode();
//return code
return Objects.hash(age,name,time);
}
@Override
public boolean equals(Object obj) {
if(this == obj) return true;
if(obj == null || getClass() != obj.getClass()) return false;
User user = (User) obj;
return age == user.age && Objects.equals(name, user.name) && Objects.equals(time, user.time);
}
}
HashMap 多线程操作导致死循环问题
主要原因在于并发下的Rehash 会造成元素之间会形成⼀个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使⽤ HashMap,因为多线程下使⽤ HashMap 还是会存在其他问题⽐如数据丢失。并发环境下推荐使⽤ ConcurrentHashMap 。
ConcurrentHashMap 如何保证线程安全,jdk1.8 有什么变化?
DK1.7:使用了分段锁机制实现 ConcurrentHashMap,ConcurrentHashMap 在对象中保存了一个 Segment 数组,即将整个 Hash 表划分为多个分段;而每个 Segment 元素,即每个分段则类似于一个 Hashtable;这样,在执行 put 操作时
首先根据 hash 算法定位到元素属于哪个 Segment,然后对该 Segment 加锁即可。因此,ConcurrentHashMap 在多线程并发编程中可是实现多线程 put 操作,不过其最大并发度受 Segment 的个数限制。
JDK1.8: 底层采用数组+链表+红黑树的方式实现,而加锁则采用 CAS 和 synchronized实现
HashMap和TreeMap应该怎么选择,使用场景
hashMap: 散列桶(数组+链表),可以实现快速的存储和检索,但是确实包含无序的元素,适用于在map中插入删除和定位元素
treeMap:使用存储结构是一个平衡二叉树->红黑树,可以自定义排序规则,要实现Comparator接口,能便捷的实现内部元素的各种排序,但是一般性能比HashMap差,适用于安装自然排序或者自定义排序规则(写过微信支付签名工具类就用这个类)
Set和Map的关系
核心就是不保存重复的元素,存储一组唯一的对象
set的每一种实现都是对应Map里面的一种封装,
HashSet对应的就是HashMap,treeSet对应的就是treeMap
Set如何解决线程不安全问题
使用CopyOnWriteSet解决
常见Map的排序规则是怎样的?
按照添加顺序使用LinkedHashMap,按照自然排序使用TreeMap,自定义排序 TreeMap(Comparetor c)
如果需要线程安全,且效率高的Map,应该怎么做?
多线程环境下可以用concurrent包下的ConcurrentHashMap, 或者使用Collections.synchronizedMap(),
ConcurrentHashMap虽然是线程安全,但是他的效率比Hashtable要高很多
ConcurrentHashMap 和 Hashtable 的区别
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的⽅式上不同。
底层数据结构: JDK1.7 的 ConcurrentHashMap 底层采⽤分段的数组+链表 实现,JDK1.8采⽤的数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑⼆叉树。 Hashtable 和JDK1.8 之前的 HashMap 的底层数据结构类似都是采⽤ 数组+链表 的形式,数组是HashMap 的主体,链表则是主要为了解决哈希冲突⽽存在的;
实现线程安全的⽅式(重要):
① 在 JDK1.7 的时候, ConcurrentHashMap (分段锁)对整个桶数组进⾏了分割分段( Segment ),每⼀把锁只锁容器其中⼀部分数据,多线程访问容器⾥不同数据段的数据,就不会存在锁竞争,提⾼并发访问率。 到了 JDK1.8 的时候已经摒弃了 Segment 的概念,⽽是直接⽤ Node 数组+链表+红⿊树的数据结构来实现,并发控制使⽤ synchronized 和 CAS来操作。(JDK1.6 以后 对 synchronized 锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap ,虽然在 JDK1.8 中还能看到Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
② Hashtable (同⼀把锁) :使⽤** synchronized** 来保证线程安全,效率⾮常低下。当⼀个线程访问同步⽅法时,其他线程也访问同步⽅法,可能会进⼊阻塞或轮询状态,如使⽤ put 添加元素,另⼀个线程不
能使⽤ put 添加元素,也不能使⽤ get,竞争会越来越激烈效率越低。
为什么Collections.synchronizedMap后是线程安全的?
使用Collections.synchronizedMap包装后返回的map是加锁的
介绍下你了解的HashMap
索引计算
索引计算方法
- 首先,计算对象的 hashCode()
- 再进行调用 HashMap 的 hash() 方法进行二次哈希
- 二次 hash() 是为了综合高位数据,让哈希分布更为均匀
- 最后 & (capacity – 1) 得到索引
数组容量为何是 2 的 n 次幂
- 计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模
- 扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap
注意
- 二次 hash 是为了配合 容量是 2 的 n 次幂 这一设计前提,如果 hash 表的容量不是 2 的 n 次幂,则不必二次 hash
- 容量是 2 的 n 次幂 这一设计计算索引效率更好,但 hash 的分散性就不好,需要二次 hash 来作为补偿,没有采用这一设计的典型例子是 Hashtable
树化与退化
树化意义
- 红黑树用来避免 DoS 攻击,防止链表超长时性能下降,树化应当是偶然情况,是保底策略
- hash 表的查找,更新的时间复杂度是 O ( 1 ) O(1) O(1),而红黑树的查找,更新的时间复杂度是 O ( l o g 2 n ) O(log_2n ) O(log2n),TreeNode 占用空间也比普通 Node 的大,如非必要,尽量还是使用链表
- hash 值如果足够随机,则在 hash 表内按泊松分布,在负载因子 0.75 的情况下,长度超过 8 的链表出现概率是 0.00000006,树化阈值选择 8 就是为了让树化几率足够小
树化规则
- 当链表长度超过树化阈值 8 时,先尝试扩容来减少链表长度,如果数组容量已经 >=64,才会进行树化
退化规则
- 情况1:在扩容时如果拆分树时,树元素个数 <= 6 则会退化链表
- 情况2:remove 树节点时,若 root、root.left、root.right、root.left.left 有一个为 null ,也会退化为链表
HashMap底层(数组+链表+红黑树 jdk8才有红黑树)
数组中每一项是一个链表,即数组和链表的结合体
Node<K,V>[] table 是数组,数组的元素是Entry(Node继承Entry),Entry元素是一个key-value的键值对,它持有一个指向下个Entry的引用,table数组的每个Entry元素同时也作为当前Entry链表的首节点,也指向了该链表的下个Entry元素
在JDK1.8中,链表的长度大于8,链表会转换成红黑树
能否解释下什么是Hash碰撞?常见的解决办法有哪些,hashmap采用哪种方法
hash碰撞的意思是不同key计算得到的Hash值相同,需要放到同个bucket中
常见的解决办法:链表法、开放地址法、再哈希法,二次寻址法等
HashMap采用的是链表法
HashMap底层是 数组+链表+红黑树,为什么要用这几类结构呢?
数组 Node<K,V>[] table ,根据对象的key的hash值进行在数组里面是哪个节点
链表的作用是解决hash冲突,将hash值一样的对象存在一个链表放在hash值对应的槽位,红黑树 JDK8使用红黑树来替代超过8个节点的链表,主要是查询性能的提升,从原来的O(n)到O(logn),通过hash碰撞,让HashMap不断产生碰撞,那么相同的key的位置的链表就会不断增长,当对这个Hashmap的相应位置进行查询的时候,就会循环遍历这个超级大的链表,性能就会下降,所以改用红黑树
为啥选择红黑树而不用其他树,比如二叉查找树,为啥不一直开始就用红黑树,而是到8的长度后才变换?
二叉查找树在特殊情况下也会变成一条线性结构,和原先的链表存在一样的深度遍历问题,查找性能就会慢,使用红黑树主要是提升查找数据的速度,红黑树是平衡二叉树的一种,插入新数据后会通过左旋,右旋、变色等操作来保持平衡,解决单链表查询深度的问题
数据量少的时候操作数据,遍历线性表比红黑树所消耗的资源少,且前期数据少平衡二叉树保持平衡是需要消耗资源的,所以前期采用线性表,等到一定数之后变换到红黑树
说下hashmap的put和get的核心逻辑(JDK8以上版本)
put:
- 判断table是否为空或者长度为0,那么则进行扩容操作
- 否则hash分析命中那个桶是否有值,没有值的话,直接保存插入
- 有值的话,判断key值是否一样,一样的话进行覆盖操作
- 不一样的话,判断是否为树节点,如果为树节点,直接插入,此时的话已经是一颗红黑树了
- 如果不是树节点,则还是链表,那么需要进行的是遍历插入
- 插入进去之后,进行判断,其长度是否大于8,大于8,则转成红黑树,不大于8的话,直接保存插入.
get: - 判断table是否为空或者长度为0
- 根据key算出bucket.然后获取首节点,hash碰撞概率小,通常链表第一个节点就是值,没必要进行遍历
- 如果不只一个值,就需要循环遍历,存在多个hash碰撞
- 然后查找的时候判断是否是红黑树,是的话则调用树的查找,如果不是则代表是链表结构,循环遍历获取节点
了解ConcurrentHashMap吗?为什么性能比hashtable高?
ConcurrentHashMap线程安全的Map, hashtable类基本上所有的方法都是采用synchronized进行线程安全控制高并发情况下效率就降低ConcurrentHashMap是采用了分段锁的思想提高性能,锁粒度更细化
jdk1.7和jdk1.8里面ConcurrentHashMap实现的区别
JDK8之前,ConcurrentHashMap使用锁分段技术,将数据分成一段段存储,每个数据段配置一把锁,即segment类,这个类继承ReentrantLock来保证线程安全
技术点:Segment+HashEntry
JKD8的版本:取消Segment这个分段锁数据结构,底层也是使用Node数组+链表+红黑树,从而实现对每一段数据就行加锁,也减少了并发冲突的概率,CAS(读)+Synchronized(写)
技术点:Node+Cas+Synchronized
说下ConcurrentHashMap的put的核心逻辑(JDK8以上版本)
不允许key或者value为空
spread(key.hashCode()) 二次哈希,减少碰撞概率
tabAt(i) 获取table中索引为i的Node元素
casTabAt(i) 利用CAS操作获取table中索引为i的Node元素
put的核心流程
1、key进行重哈希spread(key.hashCode())
2、对当前table进行无条件循环
3、如果没有初始化table,则用initTable进行初始化
4、如果没有hash冲突,则直接用cas插入新节点,成功后则直接判断是否需要扩容,然后结束
5、(fh = f.hash) == MOVED 如果是这个状态则是扩容操作,先进行扩容
6、存在hash冲突,利用synchronized (f) 加锁保证线程安全
7、如果是链表,则直接遍历插入,如果数量大于8,则需要转换成红黑树
8、如果是红黑树则按照红黑树规则插入
9、最后是检查是否需要扩容addCount()
并发
volatile、ThreadLocal的使用场景和原理
volatile原理
volatile变量进行写操作时,JVM 会向处理器发送一条 Lock 前缀的指令,将这个变量所在缓存行的数据写会到系统内存。
Lock 前缀指令实际上相当于一个内存屏障(也称内存栅栏),它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。
volatile的适用场景
1)状态标志,如:初始化或请求停机
2)一次性安全发布,如:da模式
3)独立观察,如:定期更新某个值
4)“volatile bean” 模式
5) 开销较低的“读-写锁”策略,如:计数器
ThreadLocal什么时候会出现OOM的情况?为什么?
ThreadLocal变量是维护在Thread内部的,这样的话只要我们的线程不退出,对象的引用就会一