一、Java基础
1. 值传递、引用传参
值传递、引用传递
详细答案
其实只要方法里不new,对于一般对象都是值传参
为什么说Java中只有值传递
详细答案
简单的:
1. 当运行一个main()方法的时候,会往虚拟机栈中压入一个栈帧,
即为当前栈帧(Main栈帧),用来存放main()中的局部变量表
(包括参数)、操作栈、方法出口等信息
2.而传入另一个方法中的值是在当前栈帧(main栈帧)中的
3.当执行到myTest()或者myTest2()方法时,
JVM也会往虚拟机栈栈中压入一个栈,即为myTest()栈帧或者myTest2()栈帧
这个栈帧用来存储这个方法中的局部变量信息,
所以,当前栈帧操作的数据是在当前栈帧里面,只是"值"是main栈帧的副本
4.而我们知道,同一个栈中的数据可以共享,但是不同栈中的数据不能共享
这也是为什么所有的传参都说副本传参的原因
2.类变量、成员变量和局部变量
类变量:静态变量
成员变量:类里面定义的,比如public String name ;
局部变量:局部方法里面的,只在某个范围内有效。(一般指的就是方法,语句体内)
3.自动拆装箱
自动拆装箱
什么是包装类型、什么是基本类型、什么是自动拆装箱
Integer的缓存机制,或者说常量池技术
详细答案
简单来说:
1. 基本类型有**:byte、short、char、int、long、boolean。
整形: byte, 1字节 8位(bit) 默认是0 -128~127 2^7
short, 2个字节 16 bit 2^15
int, 4字节 32bit 2^31
long 8字节 64bit
浮点型:float, 4字节 32bit 小数点只有6-7位
double 8字节 64bit
字符型:char 2字节 16bit 2^15
布尔型:boolean
2. 基本类型的包装类分别是**:Byte、Short、Character、Integer、Long、Boolean。
3. String类型
123 都实现了常量池技术,但是Double、float没有
即使实现常量池的也有限制,-128--127
为什么不用double float代表金额?
1. 我们在使用BigDecimal时,使用它的BigDecimal(String)构造器创建对象才有意义。
其他的如BigDecimal b = new BigDecimal(1)这种,还是会发生精度丢失的问题。
2.
究其原因计算机组成原理里面都有,它们的编码决定了这样的结果。
long可以准确存储19位数字,而double只能准备存储16位数字。
double由于有exp位,可以存16位以上的数字,但是需要以低位的不精确作为代价。
如果需要高于19位数字的精确存储,则必须用BigInteger来保存,当然会牺牲一些性能。
4.String StringBuffer StringBuider
速度 String < stringBuffer < StringBuilder
stringbuffer 线程安全
stringBuilder 线程不安全,所以大多stringbuiler方法都加了synchronized关键字
从字节码来看+号编译后就是使用了StringBuiler来拼接,
所以一行+++的语句就会创建一个StringBuilder,多条+++语句就会创建多个,
所以为什么建议用StringBuilder的原因。
String为什么说是不可变的?确定不可以改变吗?
详细答案
简单 答案:
1. string里面都是用final修饰的,外部无法访问,一旦初始化就确定了,
2. 重新给string赋值只是新建了一个对象,并把原来的引用指向新对象,(引用内存空间占4个字节)
3. string 也可以改变,可以反射出String对象中的char数组的属性, 进而改变通过获得的char数组的引用改变数组的结构。
string 对“+”的重载、字符串拼接的几种方式和区别
String a = "helloworld";
String b ="hello"+"world";
a==b 结果是 true
String a1 ="hello";
String a2 = "world";
String c =a1+a2;
a ==c 结果是 false
1. 可以看到,b 通过字面量直接拼接的方式并没有创建StringBuilder对象,
2. 但是c却创建了一个StringBuilder然后调用append()方法进行的字符串拼接。
因此,b的地址在常量池中,c的地址由于是new 了一个StringBuilder,
因此是在堆(Heap)中,这才导致了 a == c 的结果为 false。
总结:什么时候用+,什么时候用StringBuider,就看一个对象创建的时候是否创建了StringBuider对象,
如果用+号创建了StringBuider对象,倒不如直接用StringBuider
String 创建多少对象的问题?
String s = "a" + "b" + "c" + "d" + "e"; 只有一个
1. 因为+号左右都是常量,对于常量,编译时就直接把它们连接的结果提取出来了,
在class文件里,相当于 s ="abcde";
然后JVM执行的时候,如果常量池没有就会创建一个
String s2 =a+b+c+d+e; 创建了一个String对象
但是它会创建一个StringBuider对象,那么还会产生什么呢?
答案是一个 new char[len]字符串数组对象,而且如果len超过16,会出现 扩容的情况,如下:
int newCapacity = (value.length + 1) * 2;
+和 concat的区别
//concat的源码
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
1. +可以是字符串或者数字及其他基本类型数据,而concat只能接收字符串。
2. +左右可以为null,concat为会空指针。
3. 如果拼接空字符串,concat会稍快,在速度上两者可以忽略不计,如果拼接更多字符串建议用StringBuilder。
4. 如果+号左右都是变量,从字节码来看+号编译后,就是使用了StringBuiler来拼接,
所以一行+++的语句就会创建一个StringBuilder
多条+++语句就会创建多个,所以为什么建议用StringBuilder的原因。
5. Java支持String switch原理
case "a":
System.out.println("aa");
反编译后:
case 97: // 'a'
if(s.equals("a"))
System.out.println("aa");
break;
1. 通过对case后面的String对象调用hashCode()方法,得到一个int类型的Hash值,
然后用这个Hash值来唯一标识着这个case。
2. 那么当匹配的时候,首先调用这个字符串的hashCode()方法,
3. 获取一个Hash值(int类型),用这个Hash值来匹配所有的case,
4. 获取一个Hash值(int类型),用这个Hash值来匹配所有的case,获取一个Hash值(int类型),用这个Hash值来匹配所有的case,
5. 如果没有匹配成功,说明不存在;如果匹配成功了,接着会调用字符串的equals()方法进行匹配。
6. 由此看出,String变量不能是null;同时,switch的case子句中使用的字符串也不能为null。
6.String.intern()
String.intern()是一个Native方法,它的作用是:
如果字符常量池中已经包含一个等于此String对象的字符串,
则返回常量池中字符串的引用,否则,将新的字符串放入常量池,并返回新字符串的引用’
final String str = "bbb";
final String str1 = "bbb";
final String str2 = new String("bbb");
final String str3 = new String("bbb").intern();
System.out.println("str == str1 : "+(str == str1));
System.out.println("str == str2 : "+(str == str2));
System.out.println("str == str3 : "+(str == str3));
答案:
str == str1 : true
str == str2 : false
str == str3 : true
7. 集合类
ArrayList线程不安全,考研使用并发包下面的CopyOnWriteArrayList
使用Arrays.asList和Lists.newList使用时的陷阱
基本类型int不是对象类型,在使用Arrays.asList时,做泛型擦除时,将int[]当做对象类型,所以转成一个只有一个元素的List。在使用Arrays.asList转成的List时同样要注意,Arrays.asList返回类型为Arrays类内部定义的私有类ArrayList,并且继承与AbstractList,翻阅AbstractList源码是可以发现,是不支持add和remove操作的,也就是说Arrays.asList返回的List是个固定大小的List。
如果想把一个基本类型数组转化为List,可使用如下方法:
Arrays.asList(ArrayUtils.toObject(intArray));
hashSet为什么不能有重复数据?
怎么实现的?
- hashset 内部实现其实是new了一个hashmap,由于hashmap的key不能重复,而且hashset的value都是相同的,所以hashset不能有重复元素,查看源码我们了解到:
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
// true添加成功不存在,false添加失败存在
}
- 元素值作为的是map的key,map的value则是前面提到的PRESENT变量,这个变量只作为放入map时的一个占位符而存在,所以没什么实际用处。
- 那么重复元素会被替换吗,还是放不进去?
hashset内部实现是hashmap,hashmap相同key会发生覆盖,set也一样 - HashSet使用不当会引起内存泄漏
修改hashset中对象的属性值,且属性值是计算哈希值的字段,这时会引起内存泄漏
即:当一个对象被存储进HashSet集合中以后,就不能修改该对象的参与计算哈希值的属性值了
,否则对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中删除当前对象,造成内存泄露。
3.继承 封装 多态
类的继承: 关系可以产生一个子类,子类继承父类,
它具备了父类所有的特征,继承了父类所有的方法和变量。
重写 overwrite 覆写或覆盖 :子类可以定义新的特征,当子类需要修改父类的一些方法进行扩展,增大功能
所谓方法的重写是指子类中的方法与父类中继承的方法有完全相同的返回值类型、
方法名、参数个数以及参数类型。
重载:override 是指在一个类中,多个方法的方法名相同,但是参数列表不同。
参数列表不同指的是参数个数、参数类型或者参数的顺序不同。
实现多态主要有以下三种方式:
接口实现
2. 继承父类重写方法
3. 同一类中进行方法重载多态的三个条件:
1. 继承的存在(继承是多态的基础,没有继承就没有多态). 2. 子类重写父类的方法(多态下调用子类重写的方法). 3. 父类引用变量指向子类对象(子类到父类的类型转换). 子类转换成父类时的规则:
将一个父类的引用指向一个子类的对象,称为向上转型(upcastiog),自动进行类型转换.
此时通过父类引用调用的方法是子类覆盖或继承父类的方法,不是父类的方法.
此时通过父类引用变量无法调用子类特有的方法.
9. Unicode
Unicode 是由16位(bit)组成,即char是Unicode中的一个元素,所以unicode能表示2^15个元素
10、Java反射
1.String最终类的反射修改
2. 通过反射和工厂模式实现的IOC
3. 反序列化的时候readObject()也会用到反射
采用反射后,无论添加多少个子类,工厂类中的代码都不需要修改,只需要在操作的时候传入子类的类路径
(包名+类名)就可以了,实现了各个业务逻辑之间的完全分离,很好的降低了代码的耦合性。
使用反射机制实现的工厂模式可以通过反射取得接口的实例,但是需要传入完整的包和类名。
而且用户也无法知道一个接口有多少个可以使用的子类,所以我们通过属性文件的形式配置所需要的子类。
使用反射机制并结合属性文件的工厂模式(即Spring中的IOC)实例:
首先创建一个animal.properties的资源文件
man=com.factory.Man
dog=com.factory.Dog
11. 动态代理、静态代理、AOP
静态代理、动态代理
动态代理和反射的关系
aop使用了动态代理,其实底层也是反射
Subject subject = (Subject)Proxy.newProxyInstance(handler.getClass().getClassLoader(),
realSubject.getClass().getInterfaces(), handler);
动态代理的几种实现方式
1. JDL动态代理
2. Cglib动态代理
AOP
12. 序列化、反序列化、transient
序列化
什么是序列化与反序列化、为什么序列化、
4、JDK类库中序列化的步骤
步骤一:创建一个对象输出流,它可以包装一个其它类型的目标输出流,如文件输出流:
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\object.out"));
步骤二:通过对象输出流的writeObject()方法写对象:
oos.writeObject(new User("xuliugen", "123456", "male"));
5、JDK类库中反序列化的步骤
步骤一:创建一个对象输入流,它可以包装一个其它类型输入流,如文件输入流:
ObjectInputStream ois= new ObjectInputStream(new FileInputStream("object.out"));
步骤二:通过对象输出流的readObject()方法读取对象:
User user = (User) ois.readObject();
说明:为了正确读取数据,完成反序列化,必须保证向对象输出流写对象的顺序与从对象输入流中读对象的顺序一致。
序列化底层原理、序列化与单例模式、protobuf、为什么说序列化并不安全
1、通过对某个对象的序列化与反序列化得到的对象是一个新的对象,这就破坏了单例模式的单例性。
2. 不安全
因为序列化的对象数据转换为二进制,并且完全可逆。但是在RMI调用时
所有private字段的数据都以明文二进制的形式出现在网络的套接字上,这显然是不安全的
transient
java 的transient关键字为我们提供了便利,你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。
被transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化。
13、Java泛型
14、Java异常常见笔试题
15、阅读源码
阅读源代码
String、Integer、Long、Enum、BigDecimal、ThreadLocal、ClassLoader & URLClassLoader、ArrayList & LinkedList、 HashMap & LinkedHashMap & TreeMap & CouncurrentHashMap、HashSet & LinkedHashSet & TreeSet
16、ClassLoader、双亲委派、自定义Java.lang.XXX
JVM加载一个class时先查看是否已经加载过,没有则通过父加载器,然后递归下去,
直到BootstrapClassLoader,如果BootstrapClassloader找到了,直接返回,
如果没有找到,则一级一级返回(查看规定加载路径),最后到达自身去查找这些对象。
这种机制就叫做双亲委托。
17、类加载顺序
传送门
多线程
1、线程基础
线程的实现、线程的状态、优先级、线程调度、创建线程的多种方式、守护线程
线程与进程的区别
2、线程池
自己设计线程池、submit() 和 execute()、线程池原理
为什么不允许使用Executors创建线程池
3、锁
CAS、乐观锁与悲观锁、数据库相关锁机制、
创建一个表:t_goods 有id status name三个字段,id主键
1. 行锁,默认的模式
明确指定主键,并且有此数据,row lock
select * from t_goods where id=1 for update;
2. 明确指定主键,若查无此数据,无lock
select * from t_goods where id=3 for update;
3. 无主键 table lock
select * from t_goods where name='道具' for update;
4. 主键不明确,table lock
select * from t_goods where id>0 for update;
除了主键外,使用索引也会影响数据库的锁定级别
创建一个表:t_goods 有id status name三个字段,id主键,status 索引
1. 明确指定索引,并且有此数据,row lock
select * from t_goods where status=1 for update;
2. 明确指定索引,若查无此数据,无lock
select * from t_goods where status=3 for update;
分布式锁、偏向锁、轻量级锁、重量级锁、monitor、
锁优化、锁消除、锁粗化、自旋锁、可重入锁、阻塞锁、死锁