错题整理
------零碎回顾------
八大基本类型
-
jdk1.8中,接口中的方法可以被default和static修饰,但是!!!被修饰的方法必须有方法体
-
String str2 = new String(“aaa”) ; 一共会创建两个字符串对象一个在堆中,一个在常量池中(前提是常量池中还没有 “aaa” 字符串对象)
-
在java 中,声明一个数组时,不能直接限定数组长度,只有在创建实例化对象时,才能对给定数组长度
String a[]=new String[50];
String b[];
char c[]; -
Spring事务
Required:必须的。说明必须要有事物,没有就新建事物。
supports:支持。说明仅仅是支持事务,没有事务就非事务方式执行。
mandatory:强制的。说明一定要有事务,没有事务就抛出异常。
required_new:必须新建事物。如果当前存在事物就挂起。
not_supported:不支持事物,如果存在事物就挂起。
never:绝不有事务。如果存在事物就抛出异常 -
Object类的equals与“==”作用相同,String类重写了equals方法,只比较内容。
-
接口中的变量默认是public static final 的,方法默认是public abstract 的。
-
String是值传递,StringBuffer是引用传递。
-
线程私有和共享
-
同类A中在一个构造方法调用另一个构造方法用this(),调用普通方法用this.B().
-
StringBuffer加了同步锁,线程安全。StringBuilder线程不安全。
处理速度:StringBuilder > StringBuffer > String
少量数据用String,大量数据 单线程用StringBuilder。 -
%取模: a%b只看a的符号
-
finalize()只会在对象内存回收前被调用一次(并且要该对象重写了finalize()方法);
finalize()的调用具有不确定行,只保证方法会调用,但不保证方法里的任务会被执行完(比如一个对象手脚不够利索,磨磨叽叽,还在自救的过程中,被杀死回收了) -
所有异常和错误(Error)的基类是Throwable
-
volatile只保证线程在“加载数据阶段”加载的数据是最新的,并不能保证线程安全。
一个线程执行的过程有三个阶段:
加载(复制)主存数据到操作栈 --> 对操作栈数据进行修改 --> 将操作栈数据写回主存
volatile关键字,让编译器不去优化代码使用缓存等,以保证线程在“加载数据阶段”加载的数据都是最新的
比如:
某一时刻i=6是最新的值,volatile保证线程A,B都同时加载了这个最新的值,
然后A执行i(A)+1=7,然后将7写回主存,
B也执行i(B)+1=7,然后也将7写回内存,
这样,执行两次加法,i却只增加了1 -
int i = -5; i = ++(i++);
1.先执行括号中的i++ 在执行i++的时候 Java会将i先存放到一个临时变量中去 并返回该临时变量的值(假设为temp)
2.所以 这句可以拆成 temp = i (值为-5) 并返回temp的值 然后 i自加1 此时 i 的值为-4 但是之后 执行就会出现问题 由于返回了temp的值 继续执行的表达式为 i = ++(-5);
单目运算符无法后跟一个字面量 所以在IDEA编辑器中提示Variable expected(此处应为变量) -
行为被打断抛InterruptedException的代表方法有:
java.lang.Object 类的 wait 方法
java.lang.Thread 类的 sleep 方法
java.lang.Thread 类的 join 方法 -
JAVA一律采用Unicode编码方式,每个字符无论中英文都占两个字节。
JAVA虚拟机中通常使用UTF-16的方式保存一个字符。 -
单个与操作的符号& 用在整数上是按位与,用在布尔型变量上跟&&功能类似,但是区别是无论前面是否为真,后面必定执行
-
ThreadLocal重要作用在于多线程间的数据独立,它采用哈希表的方式来为每个线程都提供一个变量的副本;保证各个线程间的数据安全,每个线程的数据不会被另外线程访问和破坏。
-
Iterator 支持从源集合中安全地删除对象,只需在 Iterator 上调用 remove() 即可。
-
synchronized 修饰非静态方法 锁的是this对象,修饰静态方法 锁的是class对象。
-
静态内部类才可以声明静态方法
静态方法不可以使用非静态变量
抽象方法不可以有函数体 -
c=a—b,先执行a-b操作,得到c=50,再执行a减1操作,得到a=99
-
Java中的byte,short,char进行计算时都会提升为int类型。
-
java的基本编程单元是类,基本存储单元是变量。
-
类中声明的变量有默认初始值;方法中声明的变量没有默认初始值,必须在定义时初始化,否则在访问该变量时会出错。
-
~ 是位运算符,意义是 按位非(NOT)
按位非也叫做补,一元运算符NOT“~”是对其运算数的每一位取反。
仅用于整数值
反转位,即0位变为1位,1变成0
在所有情况下 〜x等于(-x)- 1 -
Java有5种方式来创建对象:
1.使用 new 关键字(最常用): ObjectName obj = new ObjectName();
2.使用反射的Class类的newInstance()方法: ObjectName obj = ObjectName.class.newInstance();
3.使用反射的Constructor类的newInstance()方法: ObjectName obj = ObjectName.class.getConstructor.newInstance();
4.使用对象克隆clone()方法: ObjectName obj = obj.clone();
5.使用反序列化(ObjectInputStream)的readObject()方法: try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_NAME))) { ObjectName obj = ois.readObject(); }
-1、深入了解JVM
0、面试必问的JVM原理
1、垃圾回收算法
两个最基本的java回收算法:复制算法和标记清理算法
复制算法:两个区域A和B,初始对象在A,继续存活的对象被转移到B。此为新生代最常用的算法
标记清理:一块区域,标记可达对象(可达性分析),然后回收不可达对象,会出现碎片,那么引出 标记-整理算法:多了碎片整理,整理出更大的内存放更大的对象
两个概念:新生代和老年代
新生代:初始对象,生命周期短的
老年代:长时间存在的对象
整个java的垃圾回收是新生代和老年代的协作,这种叫做分代回收。
P.S:Serial New收集器是针对新生代的收集器,采用的是复制算法
Parallel New(并行)收集器,新生代采用复制算法,老年代采用标记整理
Parallel Scavenge(并行)收集器,针对新生代,采用复制收集算法
Serial Old(串行)收集器,新生代采用复制,老年代采用标记整理
Parallel Old(并行)收集器,针对老年代,标记整理
CMS收集器,基于标记清理
G1收集器:整体上是基于标记 整理 ,局部采用复制
综上:新生代基本采用复制算法,老年代采用标记整理算法。cms采用标记清理。
2、逻辑运算符
按位或|
按位且&
按位取反~
按位异或^
————————————————
逻辑与&&
逻辑或||
非!
————————————————
左移<<:补0,相当于乘以2
右移>>:补符号位,相当于除以2
无符号右移>>>:补0
3、Statement
Statement 每次执行SQL语句,数据库都要执行SQL语句的编译 ,最好用于仅执行一次查询并返回结果的情形,效率高于PreparedStatement.
PreparedStatement是预编译的,使用PreparedStatement有几个好处:
a. 在执行可变参数的一条SQL时,PreparedStatement比Statement的效率高,因为DBMS预编译一条SQL当然会比多次编译一条SQL的效率要高。
b. 安全性好,有效防止SQL注入等问题。
c. 对于多次重复执行的语句,使用PreparedStament效率会更高一点,并且在这种情况下也比较适合使用batch;
d. 代码的可读性和可维护性。
CallableStatement接口扩展PreparedStatement,用来调用存储过程,它提供了对输出和输入/输出参数的支持。CallableStatement接口还具有对 PreparedStatement 接口提供的输入参数的支持。
4、wait、sleep、yield的区别
5、多线程总结
6、数组复制的效率
总结:
(1)从速度上看:System.arraycopy > clone > Arrays.copyOf > for
(2)for的速度之所以最慢是因为下标表示法每次都从起点开始寻位到指定下标处(现代编译器应该对其有进行优化,改为指针),另外就是它每一次循环都要判断一次是否达到数组最大长度和进行一次额外的记录下标值的加法运算。
(3)查看Arrays.copyOf的源码可以发现,它其实本质上是调用了System.arraycopy。之所以时间差距比较大,是因为很大一部分开销全花在了Math.min函数上了。
(4)查看System.arraycopy的源码,可以发现它实质上是通过Jni调用本地方法,及c/c++已经编译成机器码的方法,所以快。
6、Servlet
init方法: 是在servlet实例创建时调用的方法,用于创建或打开任何与servlet相的资源和初始化servlet的状态,Servlet规范保证调用init方法前不会处理任何请求
service方法:是servlet真正处理客户端传过来的请求的方法,由web容器调用,根据HTTP请求方法(GET、POST等),将请求分发到doGet、doPost等方法
destory方法:是在servlet实例被销毁时由web容器调用。Servlet规范确保在destroy方法调用之前所有请求的处理均完成,需要覆盖destroy方法的情况:释放任何在init方法中 打开的与servlet相关的资源存储servlet的状态
Servlet是线程不安全的,在Servlet类中可能会定义共享的类变量,这样在并发的多线程访问的情况下,不同的线程对成员变量的修改会引发错误。
7、原码、补码、反码
1.原码
用11和-11为例
首位是符号位 0代表正数 ,1代表负数
负数的原码是将他相反数的原码首位置为1
11的原码: 0000 0000 0000 0000 0000 0000 0000 1011
-11的原码: 1000 0000 0000 0000 0000 0000 0000 1011
2.反码
正数的反码就是它的原码
负数的反码是将它的原码除符号位,全部取反。即1变为0,0变为1。
11的反码: 0000 0000 0000 0000 0000 0000 0000 1011 与原码一致
-11的反码: 1111 1111 1111 1111 1111 1111 1111 0100 符号位不变,其余位全部取反
3.补码
正数的补码就是它的原码
负数的补码是将它的反码+1;
11的补码: 0000 0000 0000 0000 0000 0000 0000 1011 与原码一致
-11的补码: 1111 1111 1111 1111 1111 1111 1111 0101 将它的反码+1
8、三大注释
Java三大注解分别是
@Override
注解表名子类中覆盖了超类中的某个方法,如果写错了覆盖形式,编译器会报错
@Deprecated
表明不希望别人在以后使用这个类,方法,变量等等
@Suppresswarnings
达到抑制编译器产生警告的目的,但是不建议使用,因为后期编码人员看不懂编译器提示的警告,不能更好的选择更好的类去完成任务
9、Hashtable 和 HashMap 的区别:
Hashtable:
(1)Hashtable 是一个散列表,它存储的内容是键值对(key-value)映射。
(2)Hashtable 的函数都是同步的,这意味着它是线程安全的。它的key、value都不可以为null。
(3)HashTable直接使用对象的hashCode。
HashMap:
(1)由数组+链表组成的,基于哈希表的Map实现,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。
(2)不是线程安全的,HashMap可以接受为null的键(key)和值(value)。
(3)HashMap重新计算hash值
Hashtable,HashMap,Properties继承关系如下:
public class Hashtable<K,V> extends Dictionary<K,V>
implements Map<K,V>, Cloneable,
java.io.Serializable
public class HashMap<K,V>extends AbstractMap<K,V>
implements Map<K,V>, Cloneable,
Serializable
java.lang.Objecct
java.util.Dictionary<K,V>
java.util.Hashtable<Object,Object>
java.util.Properties
10、单例模式
-
定义:确保一个类只有一个实例,并提供该实例的全局访问点。
这样做的好处:有些实例,全局只需要一个就够了,使用单例模式就可以避免一个全局使用的类,频繁的创建与销毁,耗费系统资源。 -
单例模式的设计要素:
一个私有构造函数 (确保只能单例类自己创建实例)
一个私有静态变量 (确保只有一个实例)
一个公有静态函数 (给使用者提供调用方法)
简单来说就是,单例类的 构造方法不让其他人修改和使用;并且单例类自己只创建一个实例,这个实例,其他人也无法修改和直接使用;然后单例类提供一个调用方法,想用这个实例,只能调用。这样就确保了全局只创建了一次实例。
-
单例模式的6种实现及各实现的优缺点:
(一)懒汉式(线程不安全)
public class Singleton { private static Singleton uniqueInstance; private Singleton() {} public static Singleton getUniqueInstance() { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } return uniqueInstance; } }
说明:先不创建实例,当第一次被调用时,再创建实例,所以被称为懒汉式。
优点:延迟了实例化,如果不需要使用该类,就不会被实例化,节约了系统资源。
缺点:线程不安全,多线程环境下,如果多个线程同时进入了 if (uniqueInstance == null) ,若此时还未实例化,也就是uniqueInstance == null,那么就会有多个线程执行 uniqueInstance = new Singleton(); ,就会实例化多个实例;(二)饿汉式(线程安全)
public class Singleton { private static Singleton uniqueInstance = new Singleton(); private Singleton() {} public static Singleton getUniqueInstance() { return uniqueInstance; } }
说明:先不管需不需要使用这个实例,直接先实例化好实例 (饿死鬼一样,所以称为饿汉式),然后当需要使用的时候,直接调方法就可以使用了。
优点:提前实例化好了一个实例,避免了线程不安全问题的出现。
缺点:直接实例化好了实例,不再延迟实例化;若系统没有使用这个实例,或者系统运行很久之后才需要使用这个实例,都会造成系统的资源浪费。(三)加锁懒汉式(线程安全)
public class Singleton { private static Singleton uniqueInstance; private static singleton() {} //下面这个方法加锁了↓↓ private static synchronized Singleton getUinqueInstance() { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } return uniqueInstance; } }
说明:实现和 线程不安全的懒汉式 几乎一样,唯一不同的点是,在get方法上 加了一把 锁。如此一来,多个线程访问,每次只有拿到锁的的线程能够进入该方法,避免了多线程不安全问题的出现。
优点:延迟实例化,节约了资源,并且是线程安全的。
缺点:虽然解决了线程安全问题,但是性能降低了。因为,即使实例已经实例化了,既后续不会再出现线程安全问题了,但是锁还在,每次还是只能拿到锁的线程进入该方法使线程阻塞,等待时间过长。(四)双重检查锁实现(线程安全)
public class Singleton { //使用了volatile修饰↓↓ private volatile static Singleton uniqueInstance; private Singleton() {} public static Singleton getUniqueInstance() { if (uniqueInstance == null) { //check synchronized (Singleton.class) { //lock if (uniqueInstance == null) { //check uniqueInstance = new Singleton(); } } } return uniqueInstance; } }
说明:双重检查锁相当于是改进了 线程安全的懒汉式。线程安全的懒汉式 的缺点是性能降低了,造成的原因是因为即使实例已经实例化,依然每次都会有锁。而现在,我们将锁的位置变了,并且多加了一个检查。 也就是,先判断实例是否已经存在,若已经存在了,则不会执行判断方法内的有锁方法了。 而如果,还没有实例化的时候,多个线程进去了,也没有事,因为里面的方法有锁,只会让一个线程进入最内层方法并实例化实例。如此一来,最多最多,也就是第一次实例化的时候,会有线程阻塞的情况,后续便不会再有线程阻塞的问题。
为什么使用 volatile 关键字修饰了 uniqueInstance 实例变量 ?
uniqueInstance = new Singleton(); 这段代码执行时分为三步:1.为 uniqueInstance 分配内存空间 2.初始化 uniqueInstance 3.将 uniqueInstance 指向分配的内存地址
正常的执行顺序当然是 1>2>3 ,但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。
单线程环境时,指令重排并没有什么问题;多线程环境时,会导致有些线程可能会获取到还没初始化的实例。
例如:线程A 只执行了 1 和 3 ,此时线程B来调用 getUniqueInstance(),发现 uniqueInstance 不为空,便获取 uniqueInstance 实例,但是其实此时的 uniqueInstance 还没有初始化。解决办法就是加一个 volatile 关键字修饰 uniqueInstance ,volatile 会禁止 JVM 的指令重排,就可以保证多线程环境下的安全运行。
优点:延迟实例化,节约了资源;线程安全;并且相对于 线程安全的懒汉式,性能提高了。
缺点:volatile 关键字,对性能也有一些影响。(五)静态内部类实现(线程安全)
public class Singleton { private Singleton() {} private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getUniqueInstance() { return SingletonHolder.INSTANCE; } }
说明: 首先,当外部类 Singleton 被加载时,静态内部类 SingletonHolder 并没有被加载进内存。当调用 getUniqueInstance() 方法时,会运行 return SingletonHolder.INSTANCE; ,触发了 SingletonHolder.INSTANCE ,此时静态内部类 SingletonHolder 才会被加载进内存,并且初始化 INSTANCE 实例,而且 JVM 会确保 INSTANCE 只被实例化一次。
优点:延迟实例化,节约了资源;且线程安全;性能也提高了。(六)枚举类实现(线程安全)
public enum Singleton { INSTANCE; //添加自己需要的操作 public void doSomeThing() { } }
说明:默认枚举实例的创建就是线程安全的,且在任何情况下都是单例。
优点:写法简单,线程安全,天然防止反射和反序列化调用。防止反序列化:
序列化:把java对象转换为字节序列的过程;
反序列化:通过这些字节序列在内存中新建java对象的过程;
也就是说,反序列化 将一个单例实例对象写到磁盘再读回来,从而获得了一个新的实例。
我们要防止反序列化,避免得到多个实例。
枚举类天然防止反序列化。
其他单例模式 可以通过 重写 readResolve() 方法,从而防止反序列化,使实例唯一
重写 readResolve() :private Object readResolve() throws ObjectStreamException{ return singleton; }
-
单例模式的应用场景
举例:
网站计数器。
应用程序的日志应用。
Web项目中的配置对象的读取。
数据库连接池。
多线程池。
…使用场景:
频繁实例化然后又销毁的对象,使用单例模式可以提高性能。
经常使用的对象,但实例化时耗费时间或者资源多,如数据库连接池,使用单例模式,可以提高性能,降低资源损坏。
使用线程池之类的控制资源时,使用单例模式,可以方便资源之间的通信。
11、哈希冲突
解决方法:
- 开放定址法:线性探测再散列、二次探测再散列、再随机探测再散列;
- 再哈希法:换一种哈希函数;
- 链地址法 :在数组中冲突元素后面拉一条链路,存储重复的元素;
- 建立一个公共溢出区:其实就是建立一个表,存放那些冲突的元素。
什么时候会产生冲突:
HashMap中调用 hashCode() 方法来计算hashCode。
由于在Java中两个不同的对象可能有一样的hashCode,所以不同的键可能有一样hashCode,从而导致冲突的产升。
HashMap通过链地址法解决哈希冲突
HashMap底层是 数组和链表 的结合体。底层是一个线性数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。数组是 Entry[] 数组,静态内部类。 Entry就是数组中的元素,每个 Map.Entry 其实就是一个key-value对,它持有一个指向下一个元素的引用 next ,这就构成了链表。所以 很明显是链地址法。
具体过程:
当我们往HashMap中put元素的时候:当程序试图将一个key-value对放入HashMap中时,
- 程序首先根据该 key 的 hashCode() 返回值决定该 Entry 的存储位置;
- 若 Entry 的存储位置上为 null ,直接存储该对象;若不为空,两个 Entry 的 key 的 hashCode()
返回值相同,那它们的存储位置相同, - 循环遍历链表,如果这两个 Entry 的 key 通过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry 的 value,但key不会覆盖;如果这两个 Entry 的 key 通过 equals 比较返回false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部
12、抽象类和接口
13、关键字
- 48个关键字:abstract、assert、boolean、break、byte、case、catch、char、class、continue、default、do、double、else、enum、extends、final、finally、float、for、if、implements、import、int、interface、instanceof、long、native、new、package、private、protected、public、return、short、static、strictfp、super、switch、synchronized、this、throw、throws、transient、try、void、volatile、while。
- 2个保留字(现在没用以后可能用到作为关键字):goto、const。
- 3个特殊直接量:true、false、null。
14、final、finally、finalize
- final修饰变量,变量的引用(也就是指向的地址)不可变,但是引用的内容可以变(地址中的内容可变)。
- finally表示总是执行。但是其实finally也有不执行的时候,但是这个题不要扣字眼。
- 在try中调用System.exit(0),强制退出了程序,finally块不执行。
- 在进入try块前,出现了异常,finally块不执行。
- finalize方法,这个选项错就错在,这个方法一个对象只能执行一次,只能在第一次进入被回收的队列,而且对象所属于的类重写了finalize方法才会被执行。第二次进入回收队列的时候,不会再执行其finalize方法,而是直接被二次标记,在下一次GC的时候被GC。
15、垃圾收集器
新生代垃圾收集器:
-
Serial收集器
单线程收集器,收集时会暂停所有工作线程(我们将这件事情称之为Stop The World,下称STW),使用复制收集算法,虚拟机运行在Client模式时的默认新生代收集器。
-
ParNew收集器
ParNew 收集器就是Serial的多线程版本,除了使用多条收集线程外,其余行为包括算法、STW、对象分配规则、回收策略等都与Serial收集器一摸一样。对 应的这种收集器是虚拟机运行在Server模式的默认新生代收集器,在单CPU的环境中,ParNew收集器并不会比Serial收集器有更好的效果。
-
Parallel Scavenge收集器
Parallel Scavenge收集器(下称PS收集器)也是一个多线程收集器,也是使用复制算法,但它的对象分配规则与回收策略都与ParNew收集器有所不同,它是 以吞吐量最大化(即GC时间占总运行时间最小)为目标的收集器实现,它允许较长时间的STW换取总吞吐量最大化。
老年代收集器:
-
Serial Old收集器
Serial Old是单线程收集器,使用标记-整理算法,是老年代的收集器,上面三种都是使用在新生代收集器。
-
Parallel Old收集器
老年代版本吞吐量优先收集器,使用多线程和标记-整理算法,JVM 1.6提供,在此之前,新生代使用了PS收集器的话,老年代除Serial Old外别无选择,因为PS无法与CMS收集器配合工作。
-
CMS(Concurrent Mark Sweep)收集器
CMS 是一种以最短停顿时间为目标的收集器,使用CMS并不能达到GC效率最高(总体GC时间最小),但它能尽可能降低GC时服务的停顿时间,这一点对于实时或 者高交互性应用(譬如证券交易)来说至关重要,这类应用对于长时间STW一般是不可容忍的。CMS收集器使用的是标记-清除算法,也就是说它在运行期间会 产生空间碎片,所以虚拟机提供了参数开启CMS收集结束后再进行一次内存压缩。