利用自定义注解,结合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 为某子类的方法而不是擦除后的类的方法)
注解是通过@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 是一种变长编码技术,utf-8 编码中的中文占用的字节不确定,可能 2 个、3 个、4个, int 型占 4 个字节。
二叉搜索树:也称二叉查找树,或二叉排序树。定义也比较简单,要么是一颗空树,要么就是具有如下性质的二叉树:
(1)若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
(2)若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(3)任意节点的左、右子树也分别为二叉查找树;
(4)没有键值相等的节点。
平衡二叉树:在二叉搜索树的基础上多了两个重要的特点
(1)左右两子树的高度差的绝对值不能超过 1(度差可以是1,0,-1);
(2)左右两子树也是一颗平衡二叉树。
红黑书:红黑树是在普通二叉树上,对每个节点添加一个颜色属性形成的,需要同时满足以下五条性质
(1)节点是红色或者是黑色;
(2)根节点是黑色;
(3)每个叶节点(NIL 或空节点)是黑色;
(4)每个红色节点的两个子节点都是黑色的(也就是说不存在两个连续的红色节点);
(5)从任一节点到其没个叶节点的所有路径都包含相同数目的黑色节点。
区别:AVL 树需要保持平衡,但它的旋转太耗时,而红黑树就是一个没有 AVL 树那样平衡,因此插入、删除效率会高于 AVL 树,而 AVL 树的查找效率显然高于红黑树。
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 巧妙利用了磁盘预读原理,将⼀个节点的大小设为等于⼀个页(每页为 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 接口是 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 的反射机制。
Class 类与 flect 类库一起对反射的概念进行了支持,该类库包含了 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("flect.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("有参构造器 " + type);
}
}
– 运行结果: 无参构造器 Run………… 有参构造器 Run… Apple
llection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。 llections 是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、线程安全等操作。 然后还有混排(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
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,因此也被成为幽灵引用。虚引用主要用于检测对象是否已经从内存中删除。
对。
一旦变量被 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。
能。在 finally 中抛异常或者 return 会掩盖之前的异常
是的。
注意:
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表示当前类的对象
java.utilCollection是一个集合接口(集合类的一个顶级接口),它提供了对集合对象进行基本操作的通用接口方法,Collection接口在java类型中有很多具体的实现,Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接集成接口有List和Set
Collctions则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序,搜索以及线程安全等各种操作
相同点:都是返回第一个元素,并在队列中删除返回的对象
不同点:如果没有元素poll会返回null,而remove会直接抛出NoSuchElementException异常
显示转换就是类型强转,把一个大类型的数据强制赋值给小类型的数据,隐式转换就是大范围的变量能够接收小范围的数据,隐式转换和显式转换其实就是自动类型转换和强制类型转换
有指针,但是隐藏了,开发人员无法直接操作指针,由jvm来操作指针
理论上来说,java都是引用传递,对于基本数据类型,传递是值的副本,而不是值本身,对于对象类型,传递是对象的引用,当在一个方法操作参数的时候,其实操作的是引用所指向的对象
改变了,因为传递是对象的引用,操作的是引用所指向的对象
Class文件是一组以8位字节为基础单位的二进制流,各个数据项严格按顺序排列
Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这样的伪结构仅仅有两种数据类型:无符号数和表
无符号数:是基本数据类型,以U1,U2,U4,U8分别代表一个字节,两个字节,四个字节,八个字节的无符号数,能够用来描写叙述数字,索引引用,数量值或者依照UTF-8编码构成的字符串值
表:由多个无符号数或者其他表作为数据项构成的符合数据类型,全部表习惯性的以_info结尾
形参:全称为:“形式参数”,是在定义方法名和方法体的时候使用的参数,用于接收调用该方法是传入的实际值
实参:全称为"实际参数",是调用该方法时传递给该方法的实际值
静态代理:
由程序员创建或工具生成代理类的源码,再编译代理类,所谓静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和委托的关系在运行前就确定了
缺点:每个需要代理的对象都需要自己重复编写代理,很不舒服
优点:但是可以面相实际对象或者是接口的方式实现代理
动态代理:
也叫做JDK代理,接口代理,动态代理的对象,是利用JDK的API,动态的在内存中构建代理对象(是根据被代理的接口来动态生成代理类的class文件,并加载运行的过程),这就是动态代理
优点:不用关心代理类,只需要在运行阶段才指定代理哪一个对象
当前主流VM垃圾收集都采用分代收集(Fenerational Collection)算法,这种算法会根据对象存活周期的不同将内存划分为几块,如JVM中的新生代,老年代,永久代,这样就可以根据个年代特点分别采用最适合的GC算法
公共静态不可变(public static final)变量也即是我们所说的编译器常量,这里的public是可选的,实际上这些变量在编译时会被替换掉,因为编译器知道这些变量的值,并且知道这些变量在运行时不能改变,这种存在的一个问题是你使用了一个内部的或第三方库中的共有编译时常量,当时这个值后面被其他人改变了,当时你的客户端仍然在使用老的值,甚至你已经部署了一个jar,为了避免这种情况,当你在更新依赖jar文件时,确保重新编译你的程序
控制反转(IOC)是Spring框架的核心思想,用我自己的话说,就是你要做一件事,别自己可劲new了,你就说你要干啥,然后外包出去就好
依赖注入(DI)在我浅薄的想法中,就是通过接口的引用和构造方法的表达,将一些事情整好了反过来传给需要用到的地方
⾯向对象易维护、易复⽤、易扩展。 因为⾯向对象有封装、继承、多态性的特
性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,⾯向对象性能
⽐⾯向过程低。
不一样,因为内存的分配方式不一样,String str =“i” 的方式,Java虚拟机会将其分配到常量池中,而String str = new String (“i”)则会被分到堆内存中
不会,在下一个垃圾回收周期中,这个对象将是可被回收的
数据库连接是非常消耗资源的,影响到程序的性能指标,连接池是用来分配,管理释放数据库连接的,可以使应用重复使用同一个数据库连接,而不是每次都创建一个新的数据库连接连接,通过释放空闲时间较长的数据库连接避免使用数据库因为创建太多的连接而造成的连接遗漏问题,提高了程序性能
Dbcp,c3p0等,用的最多的还是c3p0,因为更加稳定,安全,通过配置文件的形式来维护数据库信息,而不是通过硬编码,当连接的数据库信息发生改变时,不需要再更改程序代码就实现了数据库信息的更新
不能,定义抽象类就是让其他类继承的,如果定义为final该类就不能被基础,这样彼此就会产生矛盾,所以final不能修饰抽象类
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:java虚拟机,运用硬件或软件手段实现的虚拟的计算机
Java虚拟机包括:寄存器,堆栈,处理器
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 是 Java Development Kit,它是功能齐全的 Java SDK。
JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟(JVM),Java 类库,java 命令和其他的⼀些基础构件。
B/S(Browser/Server),浏览器/服务器程序
C/S(Clent/Server),客户端/服务端,桌面应用程序
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 实现多态有三个必要条件: 继承、重定、向上转型,在多态中需要将子类的引用赋值给父类对象,只有这样该引用才能够具备调用父类方法和子类的方法。
Constructor 不能被 override(重写),但是可以 overload(重载),所以你可以看到⼀个类中有多个构造函数的情况。
重载Overload:表示同一个类中可以有多个名称相同的方法,但这些方法的参数列表各不相同,参数个数或类型不同
重写Override:重写就是当⼦类继承⾃⽗类的相同⽅法,输⼊数据⼀样,但要做出有别于⽗类的响应时,你就要覆盖⽗类⽅法
重写发⽣在运⾏期,是⼦类对⽗类的允许访问的⽅法的实现过程进⾏重新编写。
内存泄漏的原因很简单:
常见的内存泄漏的例子:
解决这个内存泄漏问题也很简单,将set设置为null,那就可以避免上述内存泄漏问题了,其他内存内存泄漏得一步一步分析了
内存溢出的原因:
解决:
查看程序是否存在内存泄漏的问题
设置参数加大空间
代码中是否存在死循环或者循环产生过多重复的对象实
查看是否使用nio直接操作内存
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% 左右的性能提升,但却要冒多线程不安全的⻛险
总结:
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⽅法逻辑
⼀个类的构造⽅法的作⽤是什么? 若⼀个类没有声明构造⽅法,该程序能正确执⾏吗? 为什么?
主要作用是完成对类对象的初始化⼯作。可以执⾏。因为⼀个类即使没有声明构造⽅法也会有默认的不带参数的构造⽅法。
构造⽅法有哪些特性?
两个等号,如果是基本数据类型判断的是值,引用数据类型判断的是内存地址
equals() : 它的作⽤也是判断两个对象是否相等。但它⼀般有两种使⽤情况:
情况 1:类没有覆盖 equals() ⽅法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
情况 2:类覆盖了 equals() ⽅法。⼀般,我们都覆盖 equals() ⽅法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
==hashCode() 的作⽤是获取哈希码,也称为散列码;它实际上是返回⼀个 int 整数。==这个哈希码的作⽤是确定该对象在哈希表中的索引位置。 hashCode() 定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。另外需要注意的是: Object 的 hashcode ⽅法是本地⽅法,也就是⽤ c 语⾔或 c++ 实现的,该⽅法通常⽤来将对象的 内存地址 转换为整数之后返回。
public native int hashCode();
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就
利⽤到了散列码!(可以快速找到所需要的对象)
如果两个对象相等,则 hashcode ⼀定也是相同的。两个对象相等,对两个对象分别调⽤ equals⽅法都返回 true。但是,两个对象有相同的 hashcode 值,它们也不⼀定是相等的 。因此,equals ⽅法被覆盖过,则 hashCode ⽅法也必须被覆盖。
主要⽤在三个地⽅:变量、⽅法、类。
变量:如果是基本数据类型,那么加上final字段后,其值就不能进行更改,如果是引用数据类型,那么就不能让其指向另一个对象
方法:1.锁定方法,防止继承类修改它的含义;2.是效率。在早期的 Java 实现版本中,会将 final ⽅法转为内嵌调⽤。但是如果⽅法过于庞大,可能看不到内嵌调⽤带来的任何性能提升(现在的 Java 版本已经不需要使⽤final ⽅法进⾏这些优化了)。类中所有的 private ⽅法都隐式地指定为 final。
类:加了final字段的类不允许被继承,其中所有成员方法被隐式地在指定为final方法
在 Java 中,所有的异常都有⼀个共同的祖先 java.lang 包中的 Throwable 类。 Throwable 类有两个重要的子类** Exception (异常)**和 Error (错误)。Exception 能被程序本身处理( try catch ),Error 是⽆法处理的(只能尽量避免)。
在以下 3 种特殊情况下, finally 块不会被执⾏:
Java 序列化中如果有些字段不想进⾏序列化,怎么办?
使用transient或者transient注解
transient 关键字的作⽤是:阻⽌实例中那些⽤此关键字修饰的的变量序列化;当对象被反序列化时,被transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和⽅法。
⽅法 1:通过 Scanner
Scanner sc = new Scanner(System.in);
方法2:通过BufferedReader
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
Annotation(注解)是 Java 提供的一种对元程序中元素关联信息和元数据(metadata)的途径和方法。Annatation(注解)是一个接口,程序可以通过反射来获取指定程序中元素的 Annotation对象,然后通过该 Annotation 对象来获取注解中的元数据信息。
@Target说明了Annotation所修饰的对象范围: Annotation可被用于packages、types(类、接口、枚举、Annotation 类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch 参数。在 Annotation 类型的声明中使用了 target 可更加明晰其修饰的目标
Retention 定义了该 Annotation 被保留的时间长短:表示需要在什么级别保存注解信息,用于描
述注解的生命周期(即:被描述的注解在什么范围内有效),取值(RetentionPoicy)由:
SOURCE:在源文件中有效(即源文件保留)
CLASS:在 class 文件中有效(即 class 保留)
RUNTIME:在运行时有效(即运行时保留)
@ Documented 用于描述其它类型的 annotation 应该被作为被标注的程序成员的公共 API,因此可以被例如 javadoc 此类的工具文档化。
@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 流操作要分为字节流操作和字符流操作呢?
字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就⼲脆提供了⼀个直接操作字符的接⼝,⽅便我们平时对字符进⾏流操作。如果⾳频⽂件、图⽚等媒体⽂件⽤字节流⽐较好,如果涉及到字符的话使⽤字符流⽐好。
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 模型。
浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝
深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容
如果某个方法不能按照正常的途径完成任务,就可以通过另一种路径退出方法。在这种情况下会抛出一个封装了错误信息的对象。此时,这个方法会立刻退出同时不返回任何值。另外,调用这个方法的其他代码也无法继续执行,异常处理机制会将代码执行交给异常处理器。
**Throwable **是 Java 语言中所有错误或异常的超类。下一层分为 **Error 和 Exception **
Error
Exception(RuntimeException、CheckedException)
2. Exception 又有两个分支,一个是运行时异常 RuntimeException ,一个是CheckedException。
RuntimeException 如 : NullPointerException 、 ClassCastException ;一个是检查异常CheckedException,如 I/O 错误导致的 IOException、SQLException。 RuntimeException 是那些可能在 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 "大雁";
}
});
使用CallableStatement
使用PreparedStatement类,而不是使用Statement类
保存(持久化)对象及其状态到内存或者磁盘
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 关键字阻止该变量被序列化到文件中
基于 LinkedHashMap 的访问顺序的特点,可构造一个 LRU(Least Recently Used)最近最少使用简单缓存。也有一些开源的缓存产品如 ehcache 的淘汰策略( LRU )就是在LinkedHashMap 上扩展的。
不是线程安全的;
如果有两个线程 A 和 B,都进行插入数据,刚好这两条不同的数据经过哈希计算后得到的哈希码是一样的,且该位置还有其他的数据。假设一种情况,线程 A 通讨 if 判断 ,该位置没有哈希冲突,进入了 if 语句,还没有进行数据插入,这时候 CPU 就把资源让给了线程 B,线程 A 停在了 if 语句里面,线程 B 判断该位置没有哈希冲突(线程 A 的数据还没插入),也进入了 if 语句 ,线程 B 执行完后,轮到线程 A 执行,现在线程 A 直接在该位置插入而不用再判断。这时候,你会发现线程 A 把线程 B 插入的数据给覆盖了。发生了线程不安全情况。本来在 HashMap 中,发生哈希冲突是可以用链表法或者红黑树来解决的,但是在多线程中,可能就直接给覆盖了。
通过引入单向链表来解决 Hash 冲突。当出现 Hash 冲突时,比较新老 key 值是否相等,如果相等,新值覆盖旧值。如果不相等,新值会存入新的 Node 结点,指向老节点,形成链式结构,即链表。当 Hash 冲突发生频繁的时候,会导致链表长度过长,以致检索效率低,所以 JDK1.8 之后引入了红黑树,当链表长度大于 8 时,链表会转换成红黑书,以此提高查询性能。
向 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 都是继承自 Collection 接口
List 特点:元素有放入顺序,元素可重复 ,
Set 特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉,(元素虽然无放入顺序,但是元素在 set 中的位置是有该元素的 HashCode 决定的,其位置其实是固定的,加入 Set 的 Object 必须定义 equals ()方法,另外 list 支持 for 循环,也就是通过下标来遍历,也可以用迭代器,但是 set 只能用迭代,因为他无序,无法用下标来取得想要的值。)
Set 和 List 对比
Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。List:和数组类似,List 可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变。
是的
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 透露的思想:读写分离,读和写分开 最终一致性 使用另外开辟空间的思路,来解决并发冲突
一般来讲我们使用时,会用一个线程向容器中添加元素,一个线程来读取元素,而读取的操作往往更加频繁。写操作加锁保证了线程安全,读写分离保证了读操作的效率,简直完美。如果这时候有第三个线程进行删除元素操作,读线程去读取容器中最后一个元素,读之前的时候容器大小为 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.size() - 1);
}else {
break;
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
if(list.size() <= 0){
break;
}
ve(0);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。
Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。
Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有。
Collection 是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的子类,比如 List、Set 等。
Collections 是一个包装类,包含了很多静态方法,不能被实例化,就像一个工具类,比如提供的排序方法: Collections. sort(list)。
地带起是一种设计模式,它是一个兑现,它可以遍历并选择序列中的对象,而开发人员不需要了解该序列的底层机构,迭代器通常被称为"轻量级"对象,因为创建它的代价小
可以使用Collections.unmodifiableCollection(Collection c)方法来创建一个只读集合,这样改变集合的任何操作都会抛出Java.langUnsupportedoperationException异常
Java 的 List 是非常常用的数据类型。List 是有序的 Collection。Java List 一共三个实现类:分别是 ArrayList、Vector 和 LinkedList。
ArrayList 是最常用的 List 实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数组的数据复制到新的存储空间中。当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。
Vector 与 ArrayList 一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写 Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问 ArrayList 慢。
LinkedList 是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,他还提供了 List 接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。
Set 注重独一无二的性质,该体系集合用于存储无序(存入和取出的顺序不一定相同)元素,值不能重复。对象的相等性本质是对象 hashCode 值(java 是依据对象的内存地址计算出的此序号)判断的,如果想要让两个不同的对象视为相等的,就必须覆盖 Object 的 hashCode 方法和 equals 方法。
HashSet底层由HashMap实现
HashSet的值存放于HashMap的key上
HashMap的value统一为PRESENT
哈希表边存放的是哈希值。HashSet 存储元素的顺序并不是按照存入时的顺序(和 List 显然不同) 而是按照哈希值来存的所以取数据也是按照哈希值取得。元素的哈希值是通过元素的hashcode 方法来获取的, HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较equals 方法 如果 equls 结果为 true ,HashSet 就视为同一个元素。如果 equals 为 false 就不是同一个元素。哈希值相同 equals 为 false 的元素是怎么存储呢,就是在同样的哈希值下顺延(可以认为哈希值相同的元素放在一个哈希桶中)。也就是哈希一样的存一列。
List (对付顺序的好帮⼿): 存储的元素是有序的、可重复的。
Set (注重独⼀⽆⼆的性质): 存储的元素是⽆序的、不可重复的。
**Map **(⽤ Key 来搜索的专家): 使⽤键值对(kye-value)存储,类似于数学上的函数y=f(x),“x”代表 key,"y"代表 value,Key 是⽆序的、不可重复的,value 是⽆序的、可重复的,每个键最多映射到⼀个值。
public interface RandomAccess {
}
RandomAccess 接⼝中什么都没有定义。所以,RandomAccess 接⼝不过是⼀个标识罢了。标识什么? 标识实现这个接⼝的类具有随机访问功能。在 binarySearch ⽅法中,它要判断传⼊的 list 是否RamdomAccess 的实例,如果是,调用 indexedBinarySearch() ⽅法,如果不是,那么调⽤ iteratorBinarySearch() ⽅法
HashSet 是 Set 接⼝的主要实现类 , HashSet 的底层是 HashMap ,线程不安全的,可以存储 null 值;
LinkedHashSet 是 HashSet 的⼦类,能够按照添加的顺序遍历;
TreeSet 底层使⽤红⿊树,能够按照添加元素的顺序进⾏遍历,排序的⽅式有⾃然排序和定制排
序。
Fail-Fast:一旦发现遍历的同时其他人来修改,则立刻抛出异常
Fail-Safe:发现遍历的同事其他人来修改,应当能有应对策略,例如牺牲一致性来让整个遍历运行完成
从线程安全角度:
ArrayList:底层是数组实现,线程不安全,查询和修改非常快根,根据下标就可以进行操作时间复杂度1,但是增加和删除慢,需要移动大量的元素,时间复杂度n
LinkedList: 底层是双向链表,线程不安全,查询和修改速度慢,需要进行遍历操作,时间复杂度为n,但是增加和删除速度快,时间复杂度1
Vector: 底层是数组(Object[] )实现,线程安全的,操作的时候使用synchronized进行加锁
使用场景:
Vector已经很少用了
增加和删除场景多则用LinkedList
查询和修改多则用ArrayList
方式一:自己写个包装类,根据业务一般是add/update/remove加锁
方式二:Collections.synchronizedList(new ArrayList<>()); 使用synchronized加锁
//本质还是加锁
List list2 = Collections.synchronizedList(list1);
方式三:CopyOnWriteArrayList<>() 使用ReentrantLock加锁
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
设计思想:读写分离+最终一致
缺点:内存占用问题,写时复制机制,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象,如果对象过大(大对象会直接保存在老生代)则容易发生Yong GC和Full GC
注意: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扩容 = 下次扩容的容量
HashMap、Hashtable、LinkedHashMap、TreeMap、ConcurrentHashMap
HashMap:底层是基于数组+链表,JDK8以后引入了红黑树,当链表大于8的时候,则会转成红黑树,非线程安全的,默认容量是16、允许有空的健和值
Hashtable:基于哈希表实现,线程安全的(加了synchronized),默认容量是11,不允许有null的健和值
hashcode
顶级类Object里面的方法,所有的类都是继承Object,返回是一个int类型的数
根据一定的hash规则(存储地址,字段,长度等),映射成一个数组,即散列值
equals
顶级类Object里面的方法,所有的类都是继承Object,返回是一个boolean类型
根据自定义的匹配规则,用于匹配两个对象是否一样,一般逻辑如下
//判断地址是否一样
//非空判断和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);
}
}
主要原因在于并发下的Rehash 会造成元素之间会形成⼀个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使⽤ HashMap,因为多线程下使⽤ HashMap 还是会存在其他问题⽐如数据丢失。并发环境下推荐使⽤ ConcurrentHashMap 。
DK1.7:使用了分段锁机制实现 ConcurrentHashMap,ConcurrentHashMap 在对象中保存了一个 Segment 数组,即将整个 Hash 表划分为多个分段;而每个 Segment 元素,即每个分段则类似于一个 Hashtable;这样,在执行 put 操作时
首先根据 hash 算法定位到元素属于哪个 Segment,然后对该 Segment 加锁即可。因此,ConcurrentHashMap 在多线程并发编程中可是实现多线程 put 操作,不过其最大并发度受 Segment 的个数限制。
JDK1.8: 底层采用数组+链表+红黑树的方式实现,而加锁则采用 CAS 和 synchronized实现
hashMap: 散列桶(数组+链表),可以实现快速的存储和检索,但是确实包含无序的元素,适用于在map中插入删除和定位元素
treeMap:使用存储结构是一个平衡二叉树->红黑树,可以自定义排序规则,要实现Comparator接口,能便捷的实现内部元素的各种排序,但是一般性能比HashMap差,适用于安装自然排序或者自定义排序规则(写过微信支付签名工具类就用这个类)
核心就是不保存重复的元素,存储一组唯一的对象
set的每一种实现都是对应Map里面的一种封装,
HashSet对应的就是HashMap,treeSet对应的就是treeMap
使用CopyOnWriteSet解决
按照添加顺序使用LinkedHashMap,按照自然排序使用TreeMap,自定义排序 TreeMap(Comparetor c)
多线程环境下可以用concurrent包下的ConcurrentHashMap, 或者使用Collections.synchronizedMap(),
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包装后返回的map是加锁的
索引计算方法
数组容量为何是 2 的 n 次幂
注意
树化意义
树化规则
退化规则
HashMap底层(数组+链表+红黑树 jdk8才有红黑树)
数组中每一项是一个链表,即数组和链表的结合体
Node<K,V>[] table 是数组,数组的元素是Entry(Node继承Entry),Entry元素是一个key-value的键值对,它持有一个指向下个Entry的引用,table数组的每个Entry元素同时也作为当前Entry链表的首节点,也指向了该链表的下个Entry元素
在JDK1.8中,链表的长度大于8,链表会转换成红黑树
hash碰撞的意思是不同key计算得到的Hash值相同,需要放到同个bucket中
常见的解决办法:链表法、开放地址法、再哈希法,二次寻址法等
HashMap采用的是链表法
数组 Node<K,V>[] table ,根据对象的key的hash值进行在数组里面是哪个节点
链表的作用是解决hash冲突,将hash值一样的对象存在一个链表放在hash值对应的槽位,红黑树 JDK8使用红黑树来替代超过8个节点的链表,主要是查询性能的提升,从原来的O(n)到O(logn),通过hash碰撞,让HashMap不断产生碰撞,那么相同的key的位置的链表就会不断增长,当对这个Hashmap的相应位置进行查询的时候,就会循环遍历这个超级大的链表,性能就会下降,所以改用红黑树
为啥选择红黑树而不用其他树,比如二叉查找树,为啥不一直开始就用红黑树,而是到8的长度后才变换?
二叉查找树在特殊情况下也会变成一条线性结构,和原先的链表存在一样的深度遍历问题,查找性能就会慢,使用红黑树主要是提升查找数据的速度,红黑树是平衡二叉树的一种,插入新数据后会通过左旋,右旋、变色等操作来保持平衡,解决单链表查询深度的问题
数据量少的时候操作数据,遍历线性表比红黑树所消耗的资源少,且前期数据少平衡二叉树保持平衡是需要消耗资源的,所以前期采用线性表,等到一定数之后变换到红黑树
put:
ConcurrentHashMap线程安全的Map, hashtable类基本上所有的方法都是采用synchronized进行线程安全控制高并发情况下效率就降低ConcurrentHashMap是采用了分段锁的思想提高性能,锁粒度更细化
JDK8之前,ConcurrentHashMap使用锁分段技术,将数据分成一段段存储,每个数据段配置一把锁,即segment类,这个类继承ReentrantLock来保证线程安全
技术点:Segment+HashEntry
JKD8的版本:取消Segment这个分段锁数据结构,底层也是使用Node数组+链表+红黑树,从而实现对每一段数据就行加锁,也减少了并发冲突的概率,CAS(读)+Synchronized(写)
技术点:Node+Cas+Synchronized
不允许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原理
volatile变量进行写操作时,JVM 会向处理器发送一条 Lock 前缀的指令,将这个变量所在缓存行的数据写会到系统内存。
Lock 前缀指令实际上相当于一个内存屏障(也称内存栅栏),它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。
volatile的适用场景
1)状态标志,如:初始化或请求停机
2)一次性安全发布,如:da模式
3)独立观察,如:定期更新某个值
4)“volatile bean” 模式
5) 开销较低的“读-写锁”策略,如:计数器
ThreadLocal变量是维护在Thread内部的,这样的话只要我们的线程不退出,对象的引用就会一直存在。当线程退出时,Thread类会进行一些清理工作,其中就包含ThreadLocalMa
本文发布于:2025-01-31 08:24:00,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/1738283094573773.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |