记录JAVA一些容易疏忽基础问题
基础
Q 基本变量声明注意
eg1: float f=3.4**,**错误。双精度转单精度,下转型,需强转。float f =(float)3.4; 或者写成float f =3.4F;
eg2: short s1 = 1; s1 = s1 + 1。错误,理由同上。short s1 = 1; s1 += 1。正确,因为s1+= 1;
相当于s1 = (short)(s1 + 1),其中有隐含的强制类型转换。
eg3: long w=12345678901。错误。超出了int类型的取值范围。正确: long w=12345678901L
Q 如果int x=20, y=5,则语句System.out.println(x+y +""+(x+y)+y); 的输出结果是
https://www.nowcoder.com/profile/1073556/myFollowings/detail/6908631
答案:25255
Q 如果int x=20,则语句System.out.println(x++ + ++x +x); 的输出结果是
答案:20+22+22=64,前取值在运算/先运算在取值
Q char可以保存中文?
答案:占2字节,只能保存一个中文,char c = '我'; 注意不能用""
Q.以下代码的输出结果是?
public class Test{
static{
int x=5;
}
static int x,y;
public static void main(String args[]){
x--;
myMethod( );
System.out.println(x+y+ ++x);
}
public static void myMethod( ){
y=x++ + ++x;
}
}
答案:3 ,此题坑是静态代码块中是局部变量x,不是静态变量x
Q.下面这三条语句
1
2
3
System.out.println(“is ”+ 100 + 5);
System.out.println(100 + 5 +“ is”);
System.out.println(“is ”+ (100 + 5));
的输出结果分别是? is 1005, 105 is, is 105
Q 输出结果
public class Inc {
public static void main(String[] args) {
Inc inc = new Inc();
int i = 0;
inc.fermin(i);
i= i ++;
System.out.println(i);
}
void fermin(int i){
i++;
}
}
答案:0,此题考值传递,坑 i= i ++; 先取值在运算
Q 以下代码输出的是
public class SendValue{
public String str="6";
public static void main(String[] args) {
SendValue sv=new SendValue();
sv.change(sv.str);
System.out.println(sv.str);
}
public void change(String str) {
str="10";
}
}
答案:"6" 值传递
Q 输出结果
public class StringDemo{
private static final String MESSAGE="taobao";
public static void main(String [] args) {
String a ="tao"+"bao";
String b="tao";
String c="bao";
System.out.println(a==MESSAGE);
System.out.println((b+c)==MESSAGE);
}
}
答案:true false; 编译时"tao"+"bao"将直接变成"taobao",b+c则不会优化,因为不知道在之前的步骤中bc会不会发生改变,而针对b+c则是用语法糖,新建一个StringBuilder来处理
Q.hashcode问题
两个对象相等equals
(),必须有相同的hashcode 值,反之不一定。同一对象的 hashcode 值不变
两个不相等!equals
()的对象可能会有相同的 hashcode 值,这就是为什么在 hashmap 中会有冲突。
所以其get方法是先判断hashcode相同,若相同还要判断equals。
Q 取模Hash分片,一致性Hash分片
这两种算法都是通过数据的hash来给数据分片,从而达到分治、分段、分库分表的效果。
但是一致性Hash算法,后期的扩容成本和数据迁移成本要更好。其扩容后只需要迁移少量的数据。
而取模算法扩容后,则会影响到所有的数据分片,需要大量的数据迁移,才能最终完善。
通过取模法 实现最简单的分表
index = hash(sharding字段) % 分表数量 ;
select xx from 'busy_'+index where sharding字段 = xxx;
Q.哪些是不合法的定义?
public class Demo{
float func0()
{
byte i=1;
return i;
}
float func1()
{
int i=1;
return;
}
float func2()
{
short i=2;
return i;
}
float func3()
{
long i=3;
return i;
}
float func4()
{
double i=4;
return i;
}
}
答案:func1、func4,重点是类型转换图。哪些可以自动转换,哪些需要强制转换
Q 位运算更高效
<< n 乘2的N次方 >> n 除2的N次方
Q JAVA引用类型
依次减弱
Q final关键字
其可以作用在类、方法、成员变量之上。
final成员变量是不可变的:基本变量不可变,引用变量其引用不可变,但是引用的对象内部状态是不受限制的。
final变量不可变所以其是绝对的线程安全的,也是可以保证安全发布的。
final变量必须保证最晚在构造函数中赋值/初始化,不要忘记无参构造函数也要初始化final变量
public class SimLockInfo{
private final Long sn;
private final AtomicInteger retry;
public SimLockInfo() {
//初始化final变量
retry = null;
sn = null;
}
public SimLockInfo(Integer dhhm, Long sn, String id, Boolean get, Long begintime) {
this.sn = sn;
this.retry=new AtomicInteger(0);
}
}
String
Q String类为什么是final类。
0.类安全:不会被继承修改,没有子类可以替代
1.线程安全 :不可变对象
2.支持字符串常量池数据共享,节省资源,提高效率(因为如果已经存在这个常量便不会再创建,直接拿来用)
因为是不可变对象,所以hashcode不会改变,String对象缓存了自己的hash值。这样其作为Map的Key时会有更高的效率。
Q String怎么实现不可变?
https://blog.csdn.net/u010648555/article/details/89819686
字符串是常量,它们的值在创建之后不能更改。可以看到字符串的更改相关方法是返回新的字符串实例,而不是更改当前字符串的value[], String是不可变的关键都在底层的实现,而不是只是一个final。
//部分源码
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
//拼接
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);
}
}
Q String不可变测试。
@Test
public void str(){
getstr("s",1);
}
private void getstr(String s, int i){
if (i>2)
return ;
System.out.println(i+"层递归进入前:"+s);
getstr(s+"s",i+1); //s不可变,+拼接返回new String。下层引用的是new String
System.out.println("递归返回到"+i+"层:"+s);
}
---输出结果
1层递归进入前:s
2层递归进入前:ss
递归返回到2层:ss
递归返回到1层:s
Q String常量池的意义。
减少字符串的创建减少内存开销。假如没有字符串常量池,下面这个方法每次创建方法栈帧都要实例化两个字符串,但是通过常量池就可以避免这种问题。
public void getName(){
String s="123";
String name=getInfo("NAME",s)
}
数据类型
Q 判断integer
int h=9;
double j=9.0;
Integer a=new Integer(9);
Integer b=new Integer(9);
Integer c = 9;
Integer d = 9;
Double i= 9.0;
Integer e =Integer.valueOf(9);
Integer k =Integer.valueOf("9"); //类型转换 用包装类完成
int l=Integer.valueOf(9).intValue();
Integer f = 128;
Integer g = 128;
答案:
h==j;//true
a==h;//true 自动拆箱
a==b;//false
a==c;//false
c==d;//true
c==e;//true
f==g;//false
i.equals(h)//false
i.equals(j)//true
1、基本型和基本型封装型进行“==”运算符的比较,基本型封装型将会自动拆箱变为基本型后再进行比较,因此Integer(0)会自动拆箱为int类型再进行比较,显然返回true;
int a = 220;
Integer b = 220;
System.out.println(a==b);//true
2、两个Integer类型进行“==”比较, 如果其值在-128至127 ,那么返回true,否则返回false, 这跟Integer.valueOf()的缓冲对象有关,这里不进行赘述。
Integer c=3;
Integer h=3;
Integer e=321;
Integer f=321;
System.out.println(c==h);//true
System.out.println(e==f);//false
3、两个基本型的封装型进行equals()比较,首先equals()会比较类型,如果类型相同,则继续比较值,如果值也相同,返回true。
Integer a=1;
Integer b=2;
Integer c=3;
System.out.println(c.equals(a+b));//true
4、基本型封装类型调用equals(),但是参数是基本类型,这时候,先会进行自动装箱,基本型转换为其封装类型,再进行3中的比较。
int i=1;
int j = 2;
Integer c=3;
System.out.println(c.equals(i+j));//true
Q 设有下面两个赋值语句:a,b类型?
a = Integer.parseInt("1024");
b = Integer.valueOf("1024").intValue();
答案:a和b都是整数类型变量并且它们的值相等。
Q 有如下4条语句
Integer i01 = 59;
int i02 = 59;
Integer i03 =Integer.valueOf(59);
Integer i04 = new Integer(59);
System.out.println(i01 == i02); //true Integer 会自动拆箱成 int ,然后进行值的比较
System.out.println(i01 == i03); //true Integer内部缓存
System.out.println(i03 == i04); //false
System.out.println(i02 == i04); //true Integer 会自动拆箱成 int ,然后进行值的比较
Q 敏感的小数不要使用double和float
这两种类型容易引发精度丢失,或出现比较误差,例如:1.0-0.9=0.09999999....
方案一:使用BigDecimal类型做小数计算。必须由String转化
若BigDecimal与Double等其他类型比较,请将Double转化为BigDecimal,然后用BigDecimal.compareTo做比较。
但是以上方案还有可能出现0.1与0.10比较有误差的情况,或是Double的0.1转BigDecimal后出现0.100001的情况,此时可以用减法计算,若差值小于一个阈值时认定为相等。
推荐先将double转String在转BigDecimal
https://blog.csdn.net/weixin_37864013/article/details/78232046
============类型转换================
System.out.println(new BigDecimal(1.22));
//1.2199999999999999733546474089962430298328399658203125
System.out.println(new BigDecimal(String.valueOf(1.22)));
//1.22
============计算2/3=================
float f1= 2/3;
System.out.println(f1);
//0.0 计算错误
float f= (float) (new Double(2)/3);
System.out.println(f);
//0.6666667
BigDecimal s = new BigDecimal("2");
System.out.println(s.divide(new BigDecimal("3"), 5 ,BigDecimal.ROUND_HALF_UP));
//0.66667 注意除不尽时一定要设置divide方法的保留位数和四舍五入策略,否则抛出异常
方案二:所有金额已分为单位,用long记录。
若是在遇到除不尽问题,则最后做补偿处理。
以上方案同样适用于数据库。
BigDecimal
https://blog.csdn.net/weixin_37864013/article/details/78232046
https://blog.csdn.net/u011781521/article/details/80052195
https://blog.csdn.net/haiyinshushe/article/details/82721234
(1)商业计算使用BigDecimal。
(2)尽量使用参数类型为String的构造函数。
(3) BigInteger与BigDecimal都是不可变的(immutable),在进行每一步运算时,都会产生一个新的对象,所以在做加减乘除运算时千万要保存操作后的值。
(4)所有加减乘除运算,都不是修改当前 BigDecimal对象 而是将结果作为新对象返回,与(3)相关
BigDecimal a= new BigDecimal("1.22");
a.divide(new BigDecimal(2));
System.out.println(a);
//1.22
a=a.divide(new BigDecimal(2));
System.out.println(a);
//0.61
常用API
add(BigDecimal) BigDecimal对象中的值相加,然后返回这个对象。
subtract(BigDecimal) BigDecimal对象中的值相减,然后返回这个对象。
multiply(BigDecimal) BigDecimal对象中的值相乘,然后返回这个对象。
divide(BigDecimal) BigDecimal对象中的值相除,然后返回这个对象。
toString() 将BigDecimal对象的数值转换成字符串。
doubleValue() 将BigDecimal对象中的值以双精度数返回。
floatValue() 将BigDecimal对象中的值以单精度数返回。
longValue() 将BigDecimal对象中的值以长整数返回。
intValue() 将BigDecimal对象中的值以整数返回。
类初始化
Q.以下代码的错误在哪里?
public class Test {
public static int i=1;
static {
i=2;
j=2;
System.out.println(i);
System.out.println(j);
}
public static int j=1;
}
答案: 只有System.out.println(j)会编译失败。 static代码在类加载的过程中执行, 且类加载是按照书写顺序加载代码。
"j" 在静态代码快中,只能赋值不能调用。
Q.以下代码的输出结果是?
https://www.nowcoder.com/test/question/done?tid=20172030&qid=14700#summary
public class B
{
public static B t1 = new B();
public static B t2 = new B();
{
System.out.println("构造块");
}
static
{
System.out.println("静态块");
}
public static void main(String[] args)
{
B t = new B();
}
}
答案:构造块 构造块 静态块 构造块
静态块只会在首次加载类时执行,在类第一次加载时已经确定首次,即使未执行完也是首次加载。
Q.运行下面代码,输出的结果是
class A {
public A() {System.out.println("class A");}
{ System.out.println("I'm A class"); }
static { System.out.println("class A static"); }
}
public class B extends A {
public B() { System.out.println("class B"); }
static { System.out.println("class B static"); }
{ System.out.println("I'm B class"); }
public static void main(String[] args) {
new B();
}
}
答案:只要说明父子类初始化顺序,父静-子静-父普-子普
父静态成员初始化 -- 父静态代码块 -- 子静态成员初始化 -- 子静态代码块--父成员变量初始化 -- 父代码块 --父构造器-- 子成员初始化 -- 子代码块--子构造器
class A static
class B static
I'm A class
class A
I'm B class
class B
Q.运行下面代码,输出的结果是
public class Cshsx {
private int i = 1;
public static int s = 2;
static {
System.out.println("静态代码块");
System.out.println(s);
}
{
System.out.println("代码块");
System.out.println(i);
System.out.println(s);
}
public static void main(String[] args) {
new Cshsx();
}
}
答案:主要是说明成员变量的初始化和赋值,在相应的代码块之前执行
静态代码块
2
代码块
1
2
Q.运行下面代码,输出的结果是
class X{
Y y=new Y();
public X(){
System.out.print("X");
}
}
class Y{
public Y(){
System.out.print("Y");
}
}
public class Z extends X{
Y y=new Y();
public Z(){
System.out.print("Z");
}
public static void main(String[] args) {
new Z();
}
}
答案:YXYZ
继承
Q.以下代码执行的结果显示是?
https://www.nowcoder.com/test/question/done?tid=20172030&qid=112823#summary
public class Demo {
class Super{
int flag=1;
Super(){
test();
}
void test(){
System.out.println("Super.test() flag="+flag);
}
}
class Sub extends Super{
Sub(int i){
flag=i;
System.out.println("Sub.Sub()flag="+flag);
}
void test(){
System.out.println("Sub.test()flag="+flag);
}
}
public static void main(String[] args) {
new Demo().new Sub(5);
}
}
答案:
Sub.test() flag=1
Sub.Sub() flag=5
Q.以下代码执行的结果显示是?
https://www.nowcoder.com/profile/1073556/myFollowings/detail/6618019
public class Main
{
private String baseName = "base";
public Main()
{
callName();
}
public void callName()
{
System.out.println(baseName);
}
static class Sub extends Main
{
private String baseName = "sub";
public void callName()
{
System.out.println (baseName) ;
}
}
public static void main(String[] args)
{
Main b = new Sub();
}
}
答案:null,子类构造器执行super(),由于子类中存在变量baseName和方法callName,由于动态绑定和覆盖,super()调用的是子类方法,子类方法调用子类变量。但是此时子类非静态成员变量 baseName只完成初始化null,还没有赋值"sub"。
继承中:只有子类可见的同名方法会被子类覆盖,但是父类方法还是存在的,可以在子类中通过super调用。
所以 所有的 变量+方法 都是父子相互独立共存。静态内容根据当前对象的父/子引用变量类型,调用对应类的静态内容。
非静态内容动态绑定。
Q.判断对错。在java的多态调用中,new的是哪一个类就是调用的哪个类的方法。
答案:错,动态绑定/静态绑定
final,static,private,构造器 是静态绑定方法不能覆盖
子类可见的非静态方法可以覆盖(三同一大一小原则),运用的是动态单分配,是根据new的类型确定对象,从而确定调用的方法;
静态方法同名独立共存不覆盖,运用的是静态多分派,即根据静态类型确定对象(即引用变量类型,决定调用哪个类的静态方法),因此不是根据new的类型确定调用的方法
Q.以下代码执行的结果是多少
https://www.nowcoder.com/profile/1073556/myFollowings/detail/5986970
public class Demo {
public static void main(String[] args) {
Collection<?>[] collections = {new HashSet<String>(), new ArrayList<String>(), new HashMap<String, String>().values()};
Super subToSuper = new Sub();
for(Collection<?> collection: collections) {
System.out.println(subToSuper.getType(collection));
}
}
abstract static class Super {
public static String getType(Collection<?> collection) {
return “Super:collection”;
}
public static String getType(List<?> list) {
return “Super:list”;
}
public String getType(ArrayList<?> list) {
return “Super:arrayList”;
}
public static String getType(Set<?> set) {
return “Super:set”;
}
public String getType(HashSet<?> set) {
return “Super:hashSet”;
}
}
static class Sub extends Super {
public static String getType(Collection<?> collection) {
return "Sub"; }
}
}
答案:
Super:collection
Super:collection
Super:collection
考察点1:重载静态多分派——根据传入重载方法的参数类型,选择更加合适的一个重载方法
考察点2:static方法不能被子类覆写,在子类中定义了和父类完全相同的static方法,则父类的static方法被隐藏,Son.staticmethod()或new Son().staticmethod()都是调用的子类的static方法,如果是Father.staticmethod()或者Father f = new Son(); f.staticmethod()调用的都是父类的static方法。
考察点3:此题如果都不是static方法,则最终的结果是A. 调用子类的getType,输出collection
Q.对文件名为Test.java的java代码描述正确的是()
https://www.nowcoder.com/test/question/done?tid=20172030&qid=25612#summary
class Person {
String name = "No name";
public Person(String nm) {
name = nm;
}
}
class Employee extends Person {
String empID = "0000";
public Employee(String id) {
empID = id;
}
}
public class Test {
public static void main(String args[]) {
Employee e = new Employee("123");
System.out.println(e.empID);
}
}
答案:编译报错,因为子类构造器中隐式调用super(),但是父类没有无参构造器
子类构造器中会隐式/显示调用父类构造器,但其初始化执行在子类成员变量初始化之前。父类构造器的初始化执行顺序,请看父子类初始化顺序
Q.以下代码执行的结果是?
public class Father {
protected void doSomething() {
System.out.println ("Father doSomething " );
this.doSomething();
}
public static void main(String[] args) {
Father father= new Son();
father.doSomething() ;
}
class Son extends Father {
@Override
public void doSomething() {
System.out.println ("Son ’ s doSomething " );
super.doSomething();
}
}
答案:以下代码会出现栈溢出 StackOverflow Error ,因为doSomething方法会无线循环。
Try catch finally
Q 在try的括号里面有return一个值,那在哪里执行finally里的代码?
答案:return前执行
Q下面代码运行结果是()
public class Test{
public int add(int a,int b){
try {
return a+b;
}
catch (Exception e) {
System.out.println("catch语句块");
}
finally{
System.out.println("finally语句块");
}
return 0;
}
public static void main(String argv[]){
Test test =new Test();
System.out.println("和是:"+test.add(9, 34));
}
}
答案:
finally语句块
和是:43
Q 最终输出?
public class Main {
public static int add(){
try {
return 5;
}
catch (Exception e) {
}
finally{
return 10;
}
}
public static void main(String[] args) {
System.out.println(add());
}
}
答案:10
Q 最终输出?
public class Main {
public static int add(){
int a=1;
try {
return a;
}
catch (Exception e) {
}
finally{
a=10;
}
return 0;
}
public static void main(String[] args) {
System.out.println(add());
}
}
答案:1,finally改变不了return过的基本变量
Q 最终输出?
public class Main {
public static String add(){
String a="123";
try {
return a;
}
catch (Exception e) {
}
finally{
a="456";
}
return "";
}
public static void main(String[] args) {
System.out.println(add());
}
}
答案:123 ,finally改变不了return过的引用变量
异常
Q.RuntimeException和Exception的区别
非RuntimeException即使throws上抛,最终也必需被catch处理。
RuntimeException允许不catch捕捉,方法也不用throws,其会导致代码运行中断。一般程序里自定义异常继承RuntimeException表示代码级别错误。RuntimeException一般会代表开发人员触发的错误。
public class BaseException extends RuntimeException {
private int status = 200;
public BaseException(String message,int status) {
super(message); //继承父类String message;
this.status = status;
}
getter()+setter();
}
控制台异常打印顺序
CauseBy倒序打印,最下边的CauseBy是最先触发E被打印出来的。每个CauseBy中的异常方法是顺序打印,先看第一个方法
容器(集合)
Q JAVA提供的集合都有哪些,特点是什么,使用场景?
https://blog.csdn.net/carson_ho/article/details/85043628
https://blog.csdn.net/sdgihshdv/article/details/72566485
Q Collections工具类常用API
排序
static void sort(List<T> list, Comparator<? super T> c);
static T max(Collection<? extends T> coll, Comparator<? super T> comp);
static T min(Collection<? extends T> coll, Comparator<? super T> comp);
查找
static <T> int binarySearch(List<? extends T> list, T key, Comparator<? super T> c);
复制、替换
static <T> void copy(List<? super T> dest, List<? extends T> src);
static <T> boolean replaceAll(List<T> list, T oldVal, T newVal)
杂项(无交集?、元素出现次数、List元素填充、List子串位置、翻转List、乱序List)
static boolean disjoint(Collection<?> c1, Collection<?> c2);
static <T> void fill(List<? super T> list, T obj);
static int frequency(Collection<?> c, Object o);
static int indexOfSubList(List<?> source, List<?> target);
static int lastIndexOfSubList(List<?> source, List<?> target);
static void reverse(List<?> list);
static void shuffle(List<?> list);
同步包装(包含所有类型,只以Collection为例子)
static <T> Collection<T> synchronizedCollection(Collection<T> c);
检查(包含所有类型,只以List为例子)
static <E> List<E> checkedList(List<E> list, Class<E> type);
不可变包装(包含所有类型,只以Collection为例子)
static <T> Collection<T> unmodifiableCollection(Collection<? extends T> c);
Q 若已知要产生一个要保存大量Object的容器,怎样提高代码执行效率?
答案:初始化容器时,指定初始容量,避免反复扩容。
所有以数组为基础的数据结构都涉及到扩容问题。Arrays.copyOf()
hashMap结构由于是数组+链表+红黑树,初始设置为2的N次幂。例如:2000个元素,new hashMap(128)
Q HashMap负载因子
负载因子LoadFactor:决定填充比例,默认0.75。Threshold(阈值)=LoadFactor*Capacity(map的容量) ,Threshold为Map的真实容量。
LoadFactor越大利用率越高,越小分布越均匀查找速度越快。
Q List中 subList 只是父List的视图,并不是拷贝,修改父或子的内容,相当于全部修改。
且subList 是一种内部类List。可以用List接口的引用变量。
Q 在 subList 场景中,高度注意对父集合元素个数的修改,会导致子集合的遍历、增加、
删除均会产生 ConcurrentModificationException 异常。
Q Arrays.asList()把数组转换成集合,有什么问题?
1 他返回的Arrays中的内部类其实现List接口,可以用List接口的引用变量
2 没有实现List接口的修改方法, 使用add/remove/clear 方法会抛出 UnsupportedOperationException 异常。
若想转化成ArrayList,可以new ArrayList(Arrays.asList());
3 其只是原数组的视图,不是拷贝,修改原数组,其值也改变。
str[0] = "gujin"; 那么 list.get(0)也会随之修改。
Q.List 转数组推荐不要直接用 toArray 无参方法?
因为其只能返回Object[]类,对泛型不友好
//推荐操作,保证数组类型
String[] array = new String[list.size()];
array = list.toArray(array);
//array的大小等于list时效率最高,小于则array所有为NULL,大于效率慢
Q List迭代中修改集合注意?
不要在 for/foreach 循环里进行元素的 remove/add 操作,可能会产生 ConcurrentModificationException 异常,或是难以预测的错误结果。推荐使用 Iterator/ListIterator 接口中的remove/add。注意:Iterator只能next+remove。
在Iterator遍历的过程中,一定要使用Iterator/ListIterator接口中的修改方法。集合自身的修改方法在Iterator外部使用。
Q 容器遍历 fail-fast 和 fail-safe
fail-fast:快速失败。当前线程记录遍历开始的容器修改次数modCount,遍历过程中检查有无变化。有变化说明容器被修改(可能是当前线程也可能是其他线程修改),抛出ConcurrentModificationException。
fail-safe:相当于先拷贝容器快照,遍历这个快照而不再关心容器本身的变化。缺点是可能出现弱一致性问题。JAVA并发包中的所有容器均采用此机制。
Q CopyOnWriteArrayList
适合读多写很少的情况,写操作时加锁顺序执行,复制集合,并在复制集合上添加/删除操作,然后用复制集合替换当前集合的引用。
COW注意:1.设置合理的初始容量;2.修改效率很低,最好能批量添加/删除(addAll或removeAll),可以先写到ArrayList中。
Q List 集合高效遍历?
if (list instanceof RandomAccess) {
//使用传统的for循环遍历。
} else {
//使用Iterator或者foreach。
}
Q.HashMap,自定义类型对象作为KEY,注意点?
答案:自定义类型,最好重写hashcode()和equels()。Map为提高效率会用hashcode()判断KEY是否相等。
如果不重写,默认使用Object类中hashcode()。判断的是KEY内存地址。HashMap允许KEY=NULL。
Q Map 集合高效遍历?
使用 map.entrySet +Iterator遍历 Map 类集合 KV,而不是 keySet 方式进行遍历。JDK8可以使用 Map.foreach 方法。
values()返回的是 Collection
Q Map中的视图keySet, values, entrySet****。
Map中保存着 keySet, values, entrySet三个视图。这些返回的视图是支持清除操作(删除元素后,MAP也随之改变),也可以修改元素的状态, 但是增加元素会抛出异常。因为AbstractCollection 没有实现 add 操作 但是实现了 remove 、clear 等相关操作
Q Hashmap 对比 TreeMap
综合推荐 HashMap,其结合了红黑树的优点,且修改效率相对高一些
1.HashMap 使用 hashCode+equals 实现去重的。而 TreeMap 依靠 Comparable+Comparator 来实现 Key 的去重。
2.HashMap插入和删除效率高于TreeMap。因为TreeMap调整红黑树的成本更高。
3.TreeMap的查找效率高于HashMap。因为TreeMap是纯红黑树类型。HashMap是哈希表+红黑树
Q List 集合快速去重复?
//借助Set特性和hash表, 为List去重
hashSet.addAll(arrayList);
newList.addAll(hashSet);
Q 集合交并差统计等高效操作思路与数据结构?(当内容数量不多时,也可以考虑暴力处理)
本人原文内容比较多,所以单开一页:
https://blog.csdn.net/shanchahua123456/article/details/86518172
https://blog.csdn.net/has330338724/article/details/50532901
Q Top K 问题?
其本质上是排序问题,常用思路:快速排序(求小 左递归,求大 右递归)、堆排序(适合大数据量) .。
常规排序缺点:仅仅为找出前几个元素,而将所有元素排序,浪费时间和资源
堆排序虽然本身速度没有快排和归并这种分治排序快,但是当统计外部大数据量时,求TOP K只需要建立容量为K的堆,非常节省内存开销。
JAVA中PriorityQueue 优先级队列是小根堆结构,但是可以通过comparator自定义大小,转换成大根堆模式。
PriorityQueue(int initialCapacity,Comparator<? super E> comparator)
Q 集合第三方JAR推荐?
google.Guava , Apache Commons Collections 包含很多高效的集合操作API和新类型的数据结构。
推荐Guava,经测试平均执行效率高于JDKAPI(Lists,Sets,Maps,Collections2等工具类中提供了包括集合交并差过滤等多种高效操作)。而且除了集合,还有很多高效简单的工具类,涉及String,Object,I/O等等
https://blog.csdn.net/dgeek/article/details/76221746
Q 实现LRU缓存
Map<Integer, String> map = new LinkedHashMap<Integer, String>(){
private static final long serialVersionUID = 1L;
private int maxSize = 10;
@Override
protected boolean removeEldestEntry(java.util.Map.Entry<Integer, String> pEldest) {
return size() > maxSize;
}
};
泛型
Q 集合泛型注意事项
1 List
List
List a1=new ArrayList(); //无泛型
a1.add("123");
a1.add(1);
List<Integer> a2=a1; //此处正常编译
Integer i=a2.get(0); //编译出错
假如T是E的父类,但是List
2 List<?>引用 :可以接受任意类型集合引用,但是其不能执行add元素操作。可以remove元素。
3 List<? extends T>引用 :只接受子类,上界为T(包含T)。不同于1,其引用赋值时会检查泛型约束。除了add(null)外无法执行add操作。get操作返回T类型对象。适合读
4 List<? super T>引用 :只接受父类,下界为T(包含T)。其引用赋值时会检查泛型约束。只能add添加T类或其子类。get操作返回Object类型对象。适合写
下面两段代码,可以看出
无泛型集合转换成有泛型集合,只要类型匹配就能正确的进行类型转换
有泛型集合转换另一种泛型集合,即使类型匹配,IDE也会检查出错误。
无泛型引用和任意泛型引用可以相互赋值(运行时自动转换类型,转换不匹配抛异常),有泛型引用只能与同泛型引用赋值。
@Test
public void testFx(){
List<SimCard> simCards=getsimlist();
//可以正确执行
for (SimCard s:simCards){
System.out.println(s.getDhhm());
}
}
//返回无泛型List
private List getsimlist(){
ArrayList<Object> simCards = new ArrayList(3);
simCards.add(new SimCard("1","1","1","1"));
simCards.add(new SimCard("2","1","1","1"));
return simCards;
}
@Test
public void testFx(){
//IDE提示错误
List<SimCard> simCards=getsimlist();
for (SimCard s:simCards){
System.out.println(s.getDhhm());
}
}
//返回有泛型List
private List<Object> getsimlist(){
ArrayList<Object> simCards = new ArrayList(3);
simCards.add(new SimCard("1","1","1","1"));
simCards.add(new SimCard("2","1","1","1"));
return simCards;
}
Q 泛型方法
泛型方法通过返回值引用,参数类型自动推断泛型,不需要调用时指定<T,E>。
使用时注意类型检测。
--调用
Pojo pojo =getObject("1");
--<T,E>自动推断<Pojo,String>
private <T,E> T getObject(E name){
return (T) redisTemplate.opsForValue().get(name);
}
Q 泛型测试
class TestType<T> {
private T arg;
public T getArg() {
return arg;
}
public void setArg(T arg) {
this.arg = arg;
}
public <T> T getAtgT(T name){
System.out.println(name);
return name;
}
public static void main(String[] args) {
TestType<Integer> integerTestType = new TestType<>();
integerTestType.setArg(1);
System.out.println(integerTestType.getArg());
String s= integerTestType.getAtgT("123");
Double d=integerTestType.getAtgT(new Double(1));
}
}
当integerTestType实例化后,其类泛型的属性和方法就已经绑定好。但是泛型方法getAtgT()仍然是独立的类型T。
泛型方法getAtgT()可以根据每次的参数类型,独立推断自己的泛型
事务
Q.spring的PROPAGATION_REQUIRES_NEW事务,下面哪些说法是正确的?
答案:内部事务回滚了,外部事务仍然可以提交
PROPAGATION_REQUIRES_NEW 启动一个新的, 不依赖于环境的 "内部" 事务. 这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等. 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行
Q.spring的事务注解在加synchronized锁时有没有可能出现问题
答案:要保证synchronized包裹事务方法,先进入同步锁再启动事务就不会有问题。否则因为数据库隔离性的特点引起问题。下面是有问题的代码:
@Transactional
public synchronized void update() throws Exception {
//错误用法:线程不安全,各个线程先启动自身事务,再竞争锁
}
内部类
补充:
1 局部内部类可以使用外部方法的不可变的局部变量的引用(final修饰 或 实际不变的)。这里的不可变指的是基本变量或引用变量不变,引用变量对应的Object的状态是可以改变的 。类似于闭包效果。
多线程
Q 线程安全三要素
https://blog.csdn.net/shanchahua123456/article/details/79463844
Q 下列哪个是原子操作?
A: j++ B: j = 1 C: j = j D: j = j+1
答案:B ,其他是有取值/计算/赋值 的复合操作,是线程不安全的
Q 线程1、2同时执行,有没有可能打印出“haha”?
//伪代码
x=1;
y=1;
public void a(){ //线程1 执行
x=5;
y=6;
}
public void b(){ //线程2 执行
if (y==6)&&(x<>5){
print("haha");
}
}
答案:有可能,因为jvm重排序问题。若有需要请遵守_happenbefore_原则,保证可见性和有序性
Q voliatile关键字的作用,以及happensbefore原则
https://blog.csdn.net/shanchahua123456/article/details/79463199
voliatile+cas 能够保证线程安全的三大要素。
Q ThreadLocal
https://blog.csdn.net/shanchahua123456/article/details/79464141
适合做线程级变量,起到前程隔离作用
Q 控制多个线程顺序执行
方法1:CountDownLatch或其他AQS组件 方法2 :join() 方法3:通过一个volitiale/Atomic标志变量
Q 线程JOIN,程序输出?
Thread t1=new Thread(()->{try {
Thread.sleep(5000);
System.out.print(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}},"T1");
t1.start();
t1.join();
System.out.print(Thread.currentThread().getName());
答案:T1 main ,主线程等待join线程执行完后再执行,若t1.join()时t1已经结束,则主线程继续执行
Q 并发执行时,在无锁非同步情况下,不要以类似 i==0 的判断作为参照(除非i只有0/1两态)。最好改成i<=0 这种,因为高并发情况下,可能瞬间变为负值
Q 线程状态与相互转换
图中Resource blocked,Object waiting状态,线程会进入阻塞挂起状态,并放弃当前阻塞条件对应的已持有资源(比如锁),等待其他线程唤醒自己。
Runnable 是线程状态正常可以运行,但是当前未获得CPU时间片
Time waiting相当于sleep(),此时线程进入睡眠状态,并会主动醒来,且过程中不释放已持有资源。
Q 线程挂起和唤醒?
挂起线程自身不能唤醒自己,唤醒线程A必须由其他线程完成。线程挂起后会放弃锁等资源。等被唤醒后再去尝试获取锁。
且其他线程要持有使线程A挂起的那个对象的引用。(可以通过传参或是公共变量、公共容器的方式,使其他线程获得唤醒因子)
简单理解就是用哪个Object挂起线程A,线程B就要用那个Object的唤醒方法来唤醒线程A。
使线程A挂起方式有很多,线程A可调用如下方法, 比如 objectA.wait()、conditionA.await()、CountDownLatch.await() 、lock()等待持锁 、AQS对象.acquire()等等。(AQS可以自己实现同步组件,但是首先考虑使用JDK提供的已完成组件)
线程B必须获得使线程A挂起的对应对象的引用(objectA,conditionA,AQS对象等),并调用其唤醒方法例如 objectA.notify(),AQS对象.release()。
例外还可以强制唤醒。中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞或synchronized,无能为力。
Q 什么导致线程阻塞、放弃时间片
阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪),学过操作系统的同学对它一定已经很熟悉了。Java 提供了大量方法来支持阻塞,下面让我们逐一分析。
调用Thread.sleep,Thread.yield,Thread.suspend都不会释放已持有资源(比如同步监视器锁)
obj.wait()只会释放当前线程已经持有的此obj对象的镜像锁syn(obj)
lock.condition.await()释放condition对应的Lock。操作lock.condition必须先持有lock
方法
说明
sleep()
sleep() 允许 指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时间内进入阻塞状态,不能得到CPU 时间,指定的时间一过,线程重新进入可执行状态。 典型地,sleep() 被用在等待某个资源就绪的情形:测试发现条件不满足后,让线程阻塞一段时间后重新测试,直到条件满足为止
suspend() 和 resume()
两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume() 被调用,才能使得线程重新进入可执行状态。典型地,suspend() 和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复。
yield()
yield() 使当前线程放弃当前已经分得的CPU 时间,但不使当前线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程。让同优先级的线程有执行的机会
wait() 和 notify()
调用obj的wait(), notify()方法前,必须获得obj锁。两个方法配套使用,wait() 使得线程进入阻塞状态,它有两种形式,一种允许 指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的 notify() 被调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的 notify() 被调用。
wait, nofity和nofityAll这些方法不放在Thread类当中,原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。所以把他们定义在Object类中因为锁属于对象
Lock()
等待锁的线程出现阻塞,线程释放锁后触发唤醒后续等待线程
AQS组件
线程状态控制器
https://blog.csdn.net/shanchahua123456/article/details/82780252
Q.为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用
https://blog.csdn.net/JAVAziliao/article/details/90448820
这是JDK强制的,避免出现lost wake up问题,wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的SYN锁。objcet.wait()相当于对应的此objcet镜像锁的条件Condition对象。
操作obj.wait()、obj.notify()必须先持有syn(obj)锁。
同理操作lock.condition.await()/signal()等也要先取得对应的lock,否则汇报IllegalMonitorStateException。
Q 线程wait()一定要保证在notify之前,不然线程会一直挂起。
以下代码存在什么隐患? 期望效果是main线程等待sub线程执行完任务后唤醒自己
//错误代码(有可能sub线程先执行notify,导致main一直wait)
Thread main= Thread.currentThread();
Thread sub=new Thread(()->{main.notify()}); //sub线程持有main对象
sub.start();
main.wait(); //main.wait();对应main Object对象的镜像锁
答案:可以用CountDownLatch代替,把同一个CountDownLatch交给sub线程用来唤醒main线程。因为CountDownLatch如果已减到0后,即使后执行CountDownLatch.wait()也不会挂起当前线程。 countDown()确保要被执行最好放在finally中,避免等待线程永不唤醒。线程不能唤醒自己
Q condition.await()线程被唤醒后怎样确保条件状态?
答案:最好在while循环中判断条件,保证线程醒来后,第一时间判断条件。
volitiale int i;
while (i<0){ //也可以限制重试次数,超出抛出异常,避免死循环
condition.await();
}
Q 释放/唤醒 等操作在finally中执行
最好将释放/唤醒等操作放在finally中,避免因为异常无法执行,导致另一个线程永久挂起
--下面代码存在风险,若有一个线程put时抛出异常,end.await()永远不会被唤醒
CountDownLatch end = new CountDownLatch(100);
for (int i=0;i<100;i++){
int a=i;
threadPoolExecutor.execute(() -> {
Cache.put(a); //PUT方法可能抛出RuntimeException
end.countDown();
});
}
end.await();
Q 异步任务利用Future提前暴露结果提高并行工作效率,避免重复执行
主线A将异步任务提交到线程池后返回Future,将Future放入共享缓存,线程A继续执行后续工作。其他线程先判断缓存中有无Future。已存在则直接future.get()等待结果。
Q 并发环境,避免重复执行更新缓存,防止缓存击穿
假设场景中有一个成员变量List result是DB查询结果作为本地缓存,result是懒加载当执行查询方法时判断null则查询DB结果赋值result,其他前程都直接访问缓存。怎么防止缓存击穿_,_保证只有一个线程完成DB查询,其他线程都访问缓存数据。
分析:使用LOCK虽然也能防止击穿,但是所有执行到stop的线程都要串行化持锁,即使第一个持锁线程已经更新了缓存。造成后续线程排队读缓存,影响执行效率。
//result本地缓存
//下面代码虽然能够避免击穿,但是效率低
if (result==null){
//stop 停此处线程都需要串行化取锁,效率降低
//实际上只要一个线程更新缓存后,其他线程就可以并发读缓存
synchronized (this){
//二次检查
result=getCache();
if (result==null){
//查询DB更新缓存
}
return result;
}
}
return result;
方法一:CAS自旋双重检查锁
其特点是非阻塞,不会像Lock一样将线程挂起。比较经典的例子:ConcurrentHashMap中initTable()方法。
下面是一个非常简单的例子。AtomicLong 作为非阻塞锁,利用CAS的原子性抢锁,保证同时只有一个线程能进入CAS保护的代码块。但是线程抢成功进入CAS保护代码块后,要先做二次检查判断(result==null)。因为可能出现线程2在stop位置丢失cpu时间片,此时线程1成功进入CAS更新result后又把getlock设置为0。等线程2取得CPU再次运行时其仍然可以进入CAS块,这时若不做二次检查会造成result重复更新。
--下面最简易思路
for{
查询缓存
没有则AtomicLong.CAS取得DB查询权利?
获权:二次检查缓存 (存在)?是返回:否 ->查询DB->更新缓存->放权->CountDowbLatch唤醒
无权:CountDowbLatch等待唤醒->查缓存
}
List result;
AtomicLong getlock; //==1 当前有线程加载result
List getList(){
for(;;){
if (result!=null){
return result;
}
//stop
if (getlock.compareAndSet(0, 1)){//cas保证线程安全三要素
//取得更新权利
if (result==null){
//双重检查锁
try{
result=mapper.get();
return result;
}finally{
//释放更新权利
getlock.set(0);
}
}
}
//未获得权利的线程继续FOR循环 直到取得结果
//若不想让这些线程一直FOR循环,减少CPU消耗。可以使用countDowbLatch.await()将其阻塞。等待更新result的线程将其唤醒
Thread.yield();
}
}
方法2: AtomicReference<Future> result 提前暴露结果,用CAS判断result中引用是否为null,做为竞争更新权利的锁。
Q Lock与Cas+for自旋锁
Lock会阻塞线程是线程挂起等待唤醒,Cas自旋锁不会。但是在超高并发锁的竞争十分激烈时Lock可能效果更好,因为挂起的线程不会占用CPU。所以锁的选择要在做充分的场景测试后再做取舍。
也可以CAS+CountDowbLatch使线程进入挂起状态减少CPU消耗。
Q 下面哪个行为被中断不会导致InterruptedException?
Thread.join 、Thread.sleep、Object.wait、CyclicBarrier.await、Thread.suspend、reentrantLock.lockInterruptibly()
答案:Thread.suspend 。thread1.suspend()与thread1.resume()必须成对出现。
suspend()方法就是将一个线程挂起(暂停),resume()方法就是将一个挂起线程复活继续执。
行当执行thread1.interrupt()中断方法后,会将thread1设置为中断状态。但是并不是真的中断,需要在自己的业务代码中主动判断当前线程状态:终止代码或抛出异常。
若thread1阻塞在Thread.join 、Thread.sleep、Object.wait、CyclicBarrier.await、reentrantLock.lockInterruptibly()等情况下,thread1会从阻塞中唤醒并抛出InterruptedException,。
但是thread1此时处于IO阻塞、synchronized阻塞、reentrantLock.lock()阻塞、Thread.suspend()状态下时,interrupt()中断对其没有作用,thread1会一直阻塞、等待外界唤醒。
Q 同步转异步、异步转同步思想?
同步转异步:将同步串行化的代码,提交到线程池/MQ等等,让同步串行化执行变成异步并行执行,可以缩短任务整体的用时。
异步转同步:例如线程A将一部分任务交给线程B执行,但是最终线程A需要等待B的执行结果。此时需要借助JDK的同步组件例如:Future、AQS组件、CountDownLatch等使线程A挂起,线程B执行完成后,要通过当时使线程A挂起的同步组件对象引用释放资源,唤醒线程A。
https://blog.csdn.net/shanchahua123456/article/details/85933937
https://blog.csdn.net/shanchahua123456/article/details/86744402
Q 假设如下代码中,若t1线程在t2线程启动之前已经完成启动。代码的输出是
public static void main(String[]args)throws Exception {
final Object obj = new Object();
Thread t1 = new Thread() {
public void run() {
synchronized (obj) {
try {
obj.wait();
System.out.println("Thread 1 wake up.");
} catch (InterruptedException e) {
}
}
}
};
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread() {
public void run() {
synchronized (obj) {
obj.notifyAll();
System.out.println("Thread 2 sent notify.");
}
}
};
t2.start();
}
答案:
Thread 2 sent notify.
Thread 1 wake up
Q.SimpleDateFormat 是线程安全的?
答案:不是,可以使用 ThreadLocal
如果是 JDK8 的应用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar,DateTimeFormatter 代替 SimpleDateFormat。
推荐第三方JAR包JODA-time
Q 并发生产随机数Math.random()的隐患?
答案: 会因竞争同一seed 导致的性能下降 , 推荐使用ThreadLocalRandom
Q 线程之间信息传递,线程上下文的实现和隐患?
1 通过公共对象/变量传递进行信息交互,注意线程安全的控制
2 有线程上下文的设计思想,可通过ThreadLocal实现,实现线程安全封闭+同线程各个方法间传递。
但是线程、进程之间切换、调动,注意上下文内容的传递。
例如:线程A将任务提交到线程池,线程池中的线程与线程A的线程上下文(ThreadLocal)是不同的。
或是微服务之间的调用,也涉及到线程切换的问题。可以将线程A的上下文作为参数或httpheader等手段传入消费线程。消费线程可以直接使用。或可以通过Aop/拦截器 在消费方法执行前,把上下文参数或httpheader同步到自己的同类型线程上下文中。ThreadLocal注意释放。
Q 怎么检测一个线程是否持有对象监视器
Thread类提供了一个holdsLock(Object obj):boolean方法,当且仅当对象obj的监视器被某条线程持有的时候才会返回true,注意这是一个static方法,这意味着”某条线程”指的是当前线程。
Q 主线程提交任务到线程池,此任务出现异常,主线程合适能检测到?
直到操作Future.get()时才会捕获任务抛出的异常,不操作Future.get()则主线程正常运行无异常。
public class Test1 {
@org.junit.Test
public void testThread() throws ExecutionException, InterruptedException {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 2, 1000, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2));
Test1 this1=this;
Future<String> future = threadPoolExecutor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
this1.say(); //利用局部变量 使局部内部类中拿到外部对象引用
if(true)
throw new RuntimeException("错误");
return "ok";
}
} );
System.out.println("wait....");
Thread.sleep(1000);
System.out.println("wait get...");
System.out.println(future.isDone());
System.out.println(future.get(1000,TimeUnit.SECONDS));
System.out.println("get");
}
private void say(){
System.out.println("main say");
}
}
wait....
main say
wait get...
true
java.util.concurrent.ExecutionException: java.lang.RuntimeException: 错误
Q System.currentTimeMillis()性能问题
https://www.jianshu.com/p/3fbe607600a5
并发调用此函数效率非常低那有什么策略既可以获取系统时间又避免高频率调用呢。
策略一:如果对时间精确度要求不高的话可以使用独立线程缓存时间戳。
策略二:使用Linux的clock_gettime()方法。
Java VM可以使用这个调用并且提供更快的速度currentTimeMillis()。 如果绝对必要,可以使用JNI自己实现它.
策略三:使用System.nanoTime()。
策略一实现 class MillisecondClock {
public static final MillisecondClock CLOCK = new MillisecondClock(10); private long rate = 0;// 频率 private volatile long now = 0;// 当前时间 private MillisecondClock(long rate) { this.rate = rate; this.now = System.currentTimeMillis(); start(); } private void start() { new Thread(new Runnable() { @Override public void run() { now = System.currentTimeMillis(); try { Thread.sleep(rate); } catch (InterruptedException e) { e.printStackTrace(); } } },"系统时间统一更新Thread").start(); } public long now() { return now; }
}
反射
Q 程序输出?
package test;
import java.util.Date;
public class SuperTest extends Date{
private static final long serialVersionUID = 1L;
private void test(){
System.out.println(super.getClass().getName());
}
public static void main(String[]args){
new SuperTest().test();
}
}
答案:test.SuperTest
1.首先 super.getClass() 是父类的getClass()方法,其父类是Date,它的getClass()方法是继承自Object类而且没有重写,
所以就是调用object的getClass()方法。返回当前运行时的类。
2.在调用getName()方法而getName()是:包名+类名
Q Class.forName和classloader的区别
class.forName()除了将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块。
classLoader只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象
JVM
Q.堆栈常量池
public static int INT1 =1 ;
public static int INT2 =1 ;
public static int INT3 =1 ;
局部变量:
int a1 = 1;
int a2 = 1;
int a3 = 1;
Q 常见的内存泄漏 OOM
https://blog.csdn.net/m0_38110132/article/details/81986334
https://blog.csdn.net/ZHWang102107/article/details/83247210
https://blog.csdn.net/u012516166/article/details/77014910
静态变量,容器,ThreadLocal,链接释放,非静态内部类持有外部类引用,外部循环引用,单例(因为自身引用在本类中,自己包括成员变量都无法释放)。
及时释放引用、使用非强引用.....
排查: