1.final 关键字主要用在三个地方:变量、方法、类。
对于一个 final 变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
当用 final 修饰一个类时,表明这个类不能被继承。final 类中的所有成员方法都会被隐式地指定为 final 方法。
使用 final 方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;
2.static 关键字主要有以下四种使用场景?
修饰成员变量和成员方法: 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。调用格式:类名.静态变量名 类名.静态方法名()
静态代码块: 静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—>非静态代码块—>构造方法)。 该类不管创建多少对象,静态代码块只执行一次.
静态内部类(static修饰类的话只能修饰内部类): 静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:1. 它的创建是不需要依赖外围类的创建。2. 它不能使用任何外围类的非static成员变量和方法。
静态导包(用来导入类中的静态资源,1.5之后的新特性): 格式为:import static 这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法。
3.BIO,NIO,AIO 有什么区别?
BIO (Blocking I/O): 同步阻塞 I/O 模式,数据的读取写入必须阻塞在一个线程内等待其完成。
NIO (New I/O): NIO 是一种同步非阻塞的 I/O 模型,在 Java 1.4 中引入了 NIO 框架,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。
AIO (Asynchronous I/O):异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作.
4.get和post的区别
详细介绍:
https://www.zhihu.com/question/28586791
GET产生一个TCP数据包;POST产生两个TCP数据包。
对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);
而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。
GET在浏览器回退时是无害的,而POST会再次提交请求。
GET产生的URL地址可以被Bookmark,而POST不可以。
GET请求会被浏览器主动cache,而POST不会,除非手动设置。
GET请求只能进行url编码,而POST支持多种编码方式。
GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
GET请求在URL中传送的参数是有长度限制的,而POST么有。
对参数的数据类型,GET只接受ASCII字符,而POST没有限制。
GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。
GET参数通过URL传递,POST放在Request body中。
4.Collections 工具类和 Arrays 工具类常见方法
void reverse(List list)//反转
void shuffle(List list)//随机排序
void sort(List list)//按自然排序的升序排序
void sort(List list, Comparator c)//定制排序,由Comparator控制排序逻辑
void swap(List list, int i , int j)//交换两个索引位置的元素
void rotate(List list, int distance)//旋转。当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面。
int binarySearch(List list, Object key)//对List进行二分查找,返回索引,注意List必须是有序的
int max(Collection coll)//根据元素的自然顺序,返回最大的元素。 类比int min(Collection coll)
int max(Collection coll, Comparator c)//根据定制排序,返回最大元素,排序规则由Comparatator类控制。类比int min(Collection coll, Comparator c)
void fill(List list, Object obj)//用指定的元素代替指定list中的所有元素。
int frequency(Collection c, Object o)//统计元素出现次数
int indexOfSubList(List list, List target)//统计target在list中第一次出现的索引,找不到则返回-1,类比int lastIndexOfSubList(List source, list target).
boolean replaceAll(List list, Object oldVal, Object newVal), 用新元素替换旧元素。
5.Arrays类的常见操作
排序 : sort()
查找 : binarySearch()
比较: equals()
填充 : fill()
转列表: asList()
转字符串 : toString()
复制: copyOf()
6.深拷贝 vs 浅拷贝
浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。
实现深拷贝常用的实现方式有2种:Serializable,Cloneable。
Serializable方式就是通过java对象的序列化和反序列化的操作实现对象拷贝的一种比较常见的方式。本来java对象们都待在虚拟机堆中,通过序列化,将源对象的信息以另外一种形式存放在了堆外。这时源对象的信息就存在了2份,一份在堆内,一份在堆外。然后将堆外的这份信息通过反序列化的方式再放回到堆中,就创建了一个新的对象,也就是目标对象。
public static Object cloneObjBySerialization(Serializable src)
{
Object dest = null;
try
{
ByteArrayOutputStream bos = null;
ObjectOutputStream oos = null;
try
{
bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
oos.writeObject(src);
oos.flush();
}
finally
{
oos.close();
}
byte[] bytes = bos.toByteArray();
ObjectInputStream ois = null;
try
{
ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
dest = ois.readObject();
}
finally
{
ois.close();
}
}
catch(Exception e)
{
e.printStackTrace();//克隆失败
}
return dest;
}
源对象类型及其成员对象类型需要实现Serializable接口,一个都不能少。
import java.io.Serializable;
public class BattleShip implements Serializable
{
String name;
ClonePilot pilot;
BattleShip(String name, ClonePilot pilot)
{
this.name = name;
this.pilot = pilot;
}
}
//ClonePilot类型实现了Cloneable接口,不过这对通过Serializable方式拷贝对象没有影响
public class ClonePilot implements Serializable,Cloneable
{
String name;
String sex;
ClonePilot(String name, String sex)
{
this.name = name;
this.sex = sex;
}
public ClonePilot clone()
{
try
{
ClonePilot dest = (ClonePilot)super.clone();
return dest;
}
catch(Exception e)
{
e.printStackTrace();
}
return null;
}
}
另外一种方式是Cloneable,核心是Object类的native方法clone()。通过调用clone方法,可以创建出一个当前对象的克隆体,但需要注意的是,这个方法不支持深拷贝。如果对象的成员变量是基础类型,那妥妥的没问题。但是对于自定义类型的变量,就有问题了。你会发现源对象和目标对象的自定义类型成员变量是同一个对象,也就是浅拷贝,浅拷贝就是对对象引用(地址)的拷贝。这样的话源对象和目标对象就不是彼此独立,而是纠缠不休了。为了弥补clone方法的这个不足。需要我们自己去处理非基本类型成员变量的深拷贝。
public class Cruiser implements Cloneable
{
String name;
ClonePilot pilot;
Cruiser(String name, ClonePilot pilot)
{
this.name = name;
this.pilot = pilot;
}
//Object.clone方法是protected修饰的,无法在外部调用。所以这里需要重载clone方法,改为public修饰,并且处理成员变量浅拷贝的问题。
public Cruiser clone()
{
try
{
Cruiser dest = (Cruiser)super.clone();
dest.pilot = this.pilot.clone();
return dest;
}
catch(Exception e)
{
e.printStackTrace();
}
return null;
}
}
public class ClonePilot implements Serializable,Cloneable
{
String name;
String sex;
ClonePilot(String name, String sex)
{
this.name = name;
this.sex = sex;
}
//因为所有成员变量都是基本类型,所以只需要调用Object.clone()即可
public ClonePilot clone()
{
try
{
ClonePilot dest = (ClonePilot)super.clone();
return dest;
}
catch(Exception e)
{
e.printStackTrace();
}
return null;
}
}
7.BigDecimal
浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用 equals 来判断。
float a = 1.0f - 0.9f;
float b = 0.9f - 0.8f;
System.out.println(a);// 0.100000024
System.out.println(b);// 0.099999964
System.out.println(a == b);// false
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(b);// 0.1
BigDecimal y = b.subtract(c);// 0.1
System.out.println(x.equals(y));// true
BigDecimal m = new BigDecimal("1.255433");
BigDecimal n = m.setScale(3,BigDecimal.ROUND_HALF_DOWN);
System.out.println(n);// 1.255
8.基本数据类型与包装数据类型的使用标准
强制】所有的 POJO 类属性必须使用包装数据类型。
【强制】RPC 方法的返回值和参数必须使用包装数据类型。
【推荐】所有的局部变量使用基本数据类型。
比如我们如果自定义了一个Student类,其中有一个属性是成绩score,如果用Integer而不用int定义,一次考试,学生可能没考,值是null,也可能考了,但考了0分,值是0,这两个表达的状态明显不一样.
说明 :POJO 类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何 NPE 问题,或者入库检查,都由使用者来保证。
正例 : 数据库的查询结果可能是 null,因为自动拆箱,用基本数据类型接收有 NPE 风险。
反例 : 比如显示成交总额涨跌情况,即正负 x%,x 为基本数据类型,调用的 RPC 服务,调用不成功时,返回的是默认值,页面显示为 0%,这是不合理的,应该显示成中划线。所以包装数据类型的 null 值,能够表示额外的信息,如:远程调用失败,异常退出。
9.Arrays.asList()将数组转换为集合后,底层其实还是数组
传递的数组必须是对象数组,而不是基本类型。
Arrays.asList()是泛型方法,传入的对象必须是对象数组。当传入一个原生数据类型数组时,Arrays.asList() 的真正得到的参数就不是数组中的元素,而是数组对象本身!此时List 的唯一元素就是这个数组。
如何正确的将数组转换为ArrayList?
最简便的方法(推荐)
List list = new ArrayList<>(Arrays.asList("a", "b", "c"))
使用 Java8 的Stream(推荐)
Integer [] myArray = { 1, 2, 3 };
List myList = Arrays.stream(myArray).collect(Collectors.toList());
//基本类型也可以实现转换(依赖boxed的装箱操作)
int [] myArray2 = { 1, 2, 3 };
List myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList());
Collection.toArray()方法使用的坑&如何反转数组
String [] s= new String[]{
"dog", "lazy", "a", "over", "jumps", "fox", "brown", "quick", "A"
};
List<String> list = Arrays.asList(s);
Collections.reverse(list);
s=list.toArray(new String[0]);//没有指定类型的话会报错
不要在 foreach 循环里进行元素的 remove/add 操作
泛型的实际应用
自己设计一个泛型的获取数组最小值的函数.并且这个方法只能接受Number的子类并且实现了Comparable接口。
//注意:Number并没有实现Comparable
private static <T extends Number & Comparable<? super T>> T min(T[] values) {
if (values == null || values.length == 0) return null;
T min = values[0];
for (int i = 1; i < values.length; i++) {
if (min.compareTo(values[i]) > 0) min = values[i];
}
return min;
}
自动拆装箱
对于基本类型和包装类型之间的转换,通过xxxValue()和valueOf()两个方法完成自动拆装箱,使用jad进行反编译可以看到该过程:
public class Demo {
public static void main(String[] args) {
int x = new Integer(10); // 自动拆箱
Integer y = x; // 自动装箱
}
}
反编译后结果:
public class Demo
{
public Demo(){}
public static void main(String args[])
{
int i = (new Integer(10)).intValue(); // intValue()拆箱
Integer integer = Integer.valueOf(i); // valueOf()装箱
}
}
熟悉Arrays.asList(T…)用法的小伙伴都应该知道,asList()方法传入的参数不能是基本类型的数组,必须包装成包装类型再使用,否则对应生成的列表的大小永远是1,传入基本类型的数组后,会被转换成一个二维数组,而且是**new int[1][arr.length]**这样的数组,调用list.size()当然返回1。
注解
Java中的类、接口、枚举、注解都可以看做是类类型。使用jad来看一下@interface被转换成什么:
String 类和常量池
String str1 = "abcd";//先检查字符串常量池中有没有"abcd",如果字符串常量池中没有,则创建一个,然后 str1 指向字符串常量池中的对象,如果有,则直接将 str1 指向"abcd"";
String str2 = new String("abcd");//堆中创建一个新的对象
String str3 = new String("abcd");//堆中创建一个新的对象
System.out.println(str1==str2);//false
System.out.println(str2==str3);//false
只要使用 new 方法,便需要创建新的对象
这两种不同的创建方法是有差别的。
第一种方式是在常量池中拿对象;
第二种方式是直接在堆内存空间创建一个新的对象。
String 类型的常量池比较特殊。它的主要使用方法有两种:
直接使用双引号声明出来的 String 对象会直接存储在常量池中。
如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern 方法。String.intern() 是一个 Native 方法,它的作用是:如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,JDK1.7之前(不包含1.7)的处理方式是在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用,JDK1.7以及之后的处理方式是在常量池中记录此字符串的引用,并返回该引用。
String s1 = new String("计算机");
String s2 = s1.intern();
String s3 = "计算机";
System.out.println(s2);//计算机
System.out.println(s1 == s2);//false,因为一个是堆内存中的 String 对象一个是常量池中的 String 对象,
System.out.println(s3 == s2);//true,因为两个都是常量池中的 String 对象
字符串拼接:
String str1 = "str";
String str2 = "ing"
String str3 = "str" + "ing";//常量池中的对象
String str4 = str1 + str2; //在堆上创建的新的对象
String str5 = "string";//常量池中的对象
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false
4.2 String s1 = new String(“abc”);这句话创建了几个字符串对象?
将创建 1 或 2 个字符串。如果池中已存在字符串常量“abc”,则只会在堆空间创建一个字符串常量“abc”。如果池中没有字符串常量“abc”,那么它将首先在池中创建,然后在堆空间中创建,因此将创建总共 2 个字符串对象。
验证:
String s1 = new String("abc");// 堆内存的地址值
String s2 = "abc";
System.out.println(s1 == s2);// 输出 false,因为一个是堆内存,一个是常量池的内存,故两者是不同的。
System.out.println(s1.equals(s2));// 输出 true
JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
对象的创建
Step1:类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
Step2:分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
内存分配并发问题(补充内容,需要掌握)
在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:
CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
Step3:初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
Step4:设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
Step5:执行 init 方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始, 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
如何判断一个常量是废弃常量
假如在常量池中存在字符串 “abc”,如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,“abc” 就会被系统清理出常量池。
如何判断一个类是无用的类
该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
加载该类的 ClassLoader 已经被回收。
该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
讲讲类的实例化顺序,比如父类静态数据,构造函数,字段,子类静态数据,构
造函数,字段,当 new 的时候,他们的执行顺序。
首先,会加载类并初始化静态成员(静态变量和静态初始化块)。这包括父类的静态成员和子类的静态成员。但是,重要的是要注意,静态初始化只会在类被首次加载到JVM时执行一次,而不是每次创建对象实例时。此外,父类的静态成员会先于子类的静态成员被初始化。
2.父类的非静态变量和非静态初始化块
3.父类的构造函数
4.子类的非静态变量和非静态初始化块
5.子类的构造函数