0%

1660458753851

本期总结:#

  1. bfs时,必须要记得在对第一个入队节点做vis[start]=true的操作!否则一定会导致WA!

矩阵中的局部最大值 - 力扣 (LeetCode) 竞赛

直接硬写即可,就是写8个方向的dirs数组比较累

1660458804313


https://leetcode.cn/contest/weekly-contest-306/problems/node-with-highest-edge-score/

1660458823828

看起来也很简单, 遍历一次就等得到所有点的积分了,然后再遍历一次得到最大节点(都不用写sort函数,直接遍历求最大值即可)

比较坑的是相加结果会超int。。。。

  • 2 <= n <= 10^5

    O(n^2)会超int范围,一定要注意

    Arrays.sort不支持直接long类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Solution {
    public int edgeScore(int[] edges) {
    long[][] counts = new long[edges.length][2];
    for (int i = 0; i < edges.length;i++) {
    counts[edges[i]][0] += i;
    counts[edges[i]][1] = edges[i];
    }
    Arrays.sort(counts, (a,b)->(a[0] != b[0] ? (b[0] - a[0] > 0?1:-1) : (a[1] - b[1] > 0 ? 1 : -1)));
    return (int)counts[0][1];
    }
    }

6150. 根据模式串构造最小数字 - 力扣(LeetCode)

1660458978033

我竟然一时无法冷静,看最多就9位,只想到了dfs,然后写了15分钟。。正确答案肯定不是dfs这么蠢的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Solution {
public String smallestNumber(String pattern) {
int[] nums = new int[]{1,2,3,4,5,6,7,8,9};
for (int i = 1; i <=9; i++) {
List<Integer> rest = new ArrayList<>();
Set<Integer> set = Arrays.stream(nums).boxed().collect(Collectors.toSet());
rest.add(i);
set.remove(i);
dfs(pattern, rest, set,0);
}
return min;
}
String min = null;
public void dfs(String p, List<Integer> res, Set<Integer> canSelect,int i) {
if (i == p.length()) {
String s = res.stream().map(r -> String.valueOf(r)).collect(Collectors.joining());
if (min == null || s.compareTo(min) < 0) {
min = s;
}
return;
}

int now = res.get(res.size()-1);
char sort = p.charAt(i);
Set<Integer> copySet = new HashSet<>(canSelect);
for (int select : copySet) {
if (sort == 'I') {
if (select < now) {
continue;
}

} else {
if (select > now) {
continue;
}
}
canSelect.remove(select);
res.add(select);
dfs(p, res, canSelect, i+1);
canSelect.add(select);
res.remove(res.size()-1);
}
}
}

最佳做法肯定是一次遍历搞定

有个规律

III的时候,就是选取当前最小值,逐步递增

当DDD的时候, 则应该是从DDD的最右边选当前最小值,逐步往左边递增

处理完DDD的时候更新最小值

结果这个处理也很恶心,写死我了

后来想到每次只判断I这个字母前面所存的数字,后面数字存疑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public String smallestNumber(String pattern) {

int[] results = new int[pattern.length()+1];
int num = 1;
int ri = 0;

for (int i= 0; i < pattern.length();i++) {
if (pattern.charAt(i) == 'I') {
results[ri++] = num++;
} else if (pattern.charAt(i) == 'D') {
int right = i;
// 统计D的最右边,然后从右往左处理
while (right+1 < pattern.length() && pattern.charAt(right+1) == 'D') {
right++;
}
int desLen = right - i + 1;
int highNum = num + desLen;
while (desLen-- > 0) {
results[ri++] = highNum--;
}
// 下一个注定是之前放的最小的那个
results[ri++] = num;
// num要更新
num = num + (right - i + 1) + 1;
i = right+1;
}
}
// 处理最后是III的情况
if (results[results.length-1] == 0) {
results[results.length-1] = num;
}
return Arrays.stream(results).boxed().map(r->r.toString()).collect(Collectors.joining());
}

1660462821305


6151. 统计特殊整数 - 力扣(LeetCode)

1660462871513

排列组合题

处理0的时候很恶心

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class Solution {
public int countSpecialNumbers(int n) {
String s = String.valueOf(n);
int[] nums = new int[s.length()];
for (int i = 0;i<nums.length;i++){
nums[i] = s.charAt(i) - '0';
}

boolean[] select = new boolean[10];
int canSelectCount = 10;
int result = 0;
int i = 0;
boolean flag = false;
for ( i = 0; i < s.length();i++) {

int nowCanSelect = 0;
for (int j = 0;j<nums[i];j++) {
if (!select[j]) {
nowCanSelect++;
}
}

if (i==0) {
nowCanSelect--;
}
if (!flag) {
int r = nowCanSelect * getSum(canSelectCount - 1, s.length() - i - 1);
result += r;
}

if (i > 0) {
int newR = 9*getSum(9, s.length()-i-1);
result += newR;
}
canSelectCount --;
if (select[nums[i]]) {
flag = true;
}
select[nums[i]] = true;

}
if (!flag) {
result++;
}
return result;
}

int getSum(int n, int count) {
int sum = 1;
while (count>0) {
sum *= n;
count--;
n--;
}
return sum;
}
}

暂存以下数位DP,后面做一下强化以下

1660462942718

[toc]

泛型方法#

一般定义如下,即方法的前面加了个<T>

1
2
3
public class FTest {
public <T> List<T> f(T t){...};
}

三种泛型参数的推断方式:#

  1. 直接在f()前面加确定泛型
1
fTest.<Integer>f(xxx)
  1. 通过输入参数确定, 下面这个推断为Integer
1
2
int number = 0;
fTest.f(number)
  1. 可通过 返回值 确定
1
List<Integer> list = fTest.f(xxx);

Q: 下面这段代码哪里有问题? 是toString()那里吗?#

1
2
3
4
5
public class A<T> {
public static void test(T t){
System.out.println(t.toString());
}
}

A:
test是static方法, 因此无法感知A<T>实例里的T
需要改成
public static <T> void test(T t)

toString()那里没问题,toString就是Object的方法。

泛型参数和类型消除#

Q: 泛型参数T在运行时,会变成什么?#

A: 统一变成Object且不包含任何类型信息。


Q: 泛型参数T可以可以使用instanceof做比较吗?#

1
2
3
4
5
6
class A<T> {
void f(Object arg)
if(arg instanceof T) {
...
}
}

A: 不能,编译器会报错。


Q: 泛型参数T可以进行new T()或者new T[]操作吗?#

A: 不能,编译器会报错。


Q: 能调用泛型参数对象里的方法吗?#

1
T.f();

A: 只能调用Object的方法。


Q: 可以用T做强制转化吗?#

1
T t = (T)object;

A: 能运行, 但不会真正发生转型, 编译时会触发waring警告。

新建泛型对象时的问题#

先假定有2个类, 基类Parent 和子类Child

1
2
class Parent{}
class Child extends Parent{}

回答以下问题:

Q: 下面这句话(声明list对象,左边父类泛型,右边子类泛型)有问题吗?#

1
List<Parent> list = new ArrayList<Child>()

A:
有问题,编译就错误了。 List<Parent>和ArrayList<Child>并不存在父子类的关系


Q:这个list对象声明中, ? extends Parent = Child 有什么限制?#

1
List<? extends Parent> list = new ArrayList<Child>();

A:
这个list可以调用A a = list.get(), 但是不能list.add(new Parent())

  • 原因:
    list.get()所做的操作是在返回时, 把内部的<? extend Parent> 强转成Parent, 是合理的,任何Parent的子类都可以转成Parent
    list.add(new Parent())所做的操作是在输入时, 把外部的A转成内部的<? extend Parent>, 这是不合理的,因为我们不知道这个Parent对象可以转成哪个Parent的子类。

Q:这个list对象声明中,? super Child = Parent 有什么特点?#

1
List<? super Child> list = new ArrayList<Parent>();

下面谁会报错

1
2
3
4
list.add(new Child())
list.add(new Parent())
Parent a= list.get();
Child b = list.get()

A:
截图如下:
image.png

  • Child c = list.get() 或者Parent p = list.get()所做的操作是在返回时, 把内部的<? super Child> 强转成外部的Parent或者child, 是不合理的, 因为编译器觉得child的父类 不一定 能转成parent或者child,所以禁止了这种行为( 比如parent的父类是object, 但object不一定就能转成parent或者child)
    *list.add(new Child())所做的操作是在输入时, 把外部的child或者parent转成内部的<? super Child>, 这是合理的,因为child和parent一定能转成child的父类。

Q:List<?> list = new ArrayList#

A:
get和add都不行,只能做remove等无返回值无输入A的操作。
PS: 注意,不是说不能调用get或add方法, 而是调用get或add时,不能使用A这个对象去操作。
即无法做add(A) 或者 A a = get(0)
但是可以做add(object) 或者Object o = get(0)
因为?可以转为Object, 但是无法转为A。


Q:下面这个代码会报错吗?#

1
2
3
4
5
6
List<Fruit> fruitList = new ArrayList<>();
fruitList.add(new Fruit());
List<Apple> appleList = new ArrayList<>();
appleList.add(new Apple());
fruitList.addAll(appleList);
System.out.println(fruitList);

A:
不会报错。会正常打印结果。
image.png


PECS原则#

注意PECS原则和上面的区别!
上面之前提到的? extend或者? supert, 都是在声明对象的时候用的。
而PECS原则是用于泛型对象的方法输入参数!

假设有一个类定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static class MyList<T> {
List<T> list = new ArrayList<>();

// 把输入参数塞给自己,类似于生产操作
public void pushList(List<T> t) {
list.addAll(t);
}

// 把自己的内容塞给输入参数,类似于让输入参数做消费。
public void pollList(List<T> t) {
t.addAll(list);
}
}

则T就是泛型参数。

Q:下面代码能正常运行吗?#

1
2
3
4
5
6
7
MyList<Number> myList = new MyList<>();

List<Integer> intList = new ArrayList<>();
myList.pushList(intList);

List<Object> objectList = new ArrayList<>();
myList.pollList(objectList);

A:
不能正常运行, pushList和pollList都会报错
因为编译器检查后,认为 List<Integer>和List<Number>不是一个东西!


Q: 如果上文要支持pushList,应该怎么修改pushList方法的定义?#

A:
改成这样:

1
2
3
4
// 把输入参数塞给自己,类似于生产操作
public void pushList(List<? extends T> t) {
list.addAll(t);
}

即编译器认为,List<Integer> 和List<? extend Number>是一个东西,允许!


Q: 如果要支持pollList,怎么修改定义?#

A:

1
2
3
4
// 把自己的内容塞给输入参数,类似于让输入参数做消费。
public void pollList(List<? super T> t) {
t.addAll(list);
}

因为是把自己的东西塞给输入参数, 而想要能塞进去,必须保证自己这个T,是输入参数的子类,反过来说,输入参数必须是T的父类,所以用super
于是编译器认为,List<Object> 和List<? super Number>是一个东西,允许!


PECS原则出自Effective Java, 注意只是一个编程建议而已!#

  • 如果有一个类A,泛型参数为T
  • 如果他一般只用于接收输入容器List后,塞入自己内部的T容器, 则类A就叫生产者, 因此输入参数最好定义为<? extend T>最好, 以便能接收任何T子类的容器。
  • 如果他一般只用于接收输入容器后List, 把自己内部的T元素塞给它, 那么这个类A就叫消费者, 输入参数最好定义为<? super T>\ 最好, 以便自己的T元素能塞给任何T元素的父类容器。

[toc]

异常体系分类#

Q: Throwable 和 Error的关系#

A: Throwable是Error(错误)的基类,也是Exception的基类
1个好图,可看到常见的异常和error
image.png


Q: Error和Exception的关系#

A:

  • Error一般是会直接引起jvm出错的错误,例如Java虚拟机运行错误等,如果出现了当前线程会无法继续运行。
  • Excpetion是程序本身可以处理的异常。发生后还能正常运行。

Q: Error可以被catch捕捉吗?#

A: 只要是Throwable和其子类都是可以throw和catch的。 但是不建议捕捉Error。


异常体系还可以分为这2类:#

  • unchecked exception(非检查异常)
    也称运行时异常(RuntimeException),比如常见的NullPointerException、IndexOutOfBoundsException。对于运行时异常,java编译器不要求必须进行异常捕获处理或者抛出声明,由程序员自行决定。

  • checked exception(检查异常,编译异常)
    也称非运行时异常(运行时异常以外的异常就是非运行时异常),java编译器强制程序员必须进行捕获处理,比如常见的IOExeption和SQLException。对于非运行时异常如果不进行捕获或者抛出声明处理,编译都不会通过。

异常捕捉和返回#

Q: return-finally陷阱1: finally能通过修改变量,来更新return的变量值吗#

1
2
3
4
5
6
7
8
9
int f() {
int a = 1;
try {
return a;
}
finally {
a=2;
}
}

A: 不能, f返回1。


Q: return-finally陷阱2: finally里也return时,返回哪个?#

1
2
3
4
5
6
7
8
int f() {
try {
return 1;
}
finally {
return 2;
}
}

A:返回finally里的,返回2。


Q: 什么情况下finally块里的步骤可以不执行?#

A: 只有在finally之前调用System.exit(0)退出jvm, 才能让finally不执行。


Q: 先捕捉父类异常,再捕捉子类异常,会发生什么?#

1
2
3
4
5
6
7
try {
start();
} catch (Exception ex) {
System.out.println("catch Exception");
} catch (RuntimeException re) {
System.out.println("catch RuntimeException");
}

A: 直接编译就错误了。 catch是会按顺序的且匹配1个就不再往下匹配,编译器因此识别出RuntimeExcpetion永远不会被捕捉到,便提前报错。


Q:throw异常的时候,在finally中做return,那么异常还会抛出吗?#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static int f() {
try {
int a = 1/0;
return a;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
return -1;
}
}


public static void main(String[] args) {
System.out.println(f());
}

A:
不会,返回-1.
即finaly中做return会中断throw
因此永远不要在finally中去做return操作

受检异常相关问题#

Q: 子类覆写基类方法时 , 能throws基类方法中不存在的异常吗?#

像下面这样:

1
2
3
4
5
6
7
8
class A{
void f() throws IOException{
}
}
class B extends A{
void f() throws IOException, SQLException {
}
}

A: 不行,直接编译报错。 即子类覆写父类方法时, throws关键字后面跟的异常必须是小于等于父类方法异常的。
image.png


Q: finally中调用某资源的close时,也会抛出受检异常, 除了在finally里做try-catch,还能怎么做?#

像下面这样,finally又有catch,就很难看:

1
2
3
4
5
6
7
8
9
10
11
12
TryWithResource tryWithResource = new TryWithResource();
try {
System.out.println(tryWithResource.age);
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
tryWithResource.close();
} catch (Exception e) {
e.printStackTrace();
}
}

A:如果是JDK1.7,可以用try-with-resource语法。
需要资源类实现AutoCloseable接口, 并在try的时候在try括号后面跟上资源的创建,如下:

1
2
3
4
5
6
7
public static void main(String[] args) {
try (TryWithResource tryWithResource = new TryWithResource()) {
System.out.println(tryWithResource.age);
} catch (Exception e) {
e.printStackTrace();
}
}

这样就不需要写finally,finally+close会通过编译器给我们自动加上。


Q: 线程抛出异常的话该怎么捕捉?#

A: 实现异常处理接口MyUnchecckedExceptionhandler

1
2
3
4
5
6
public class MyUnchecckedExceptionhandler implements UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("捕获异常处理方法:" + e);
}
}

然后把实现类设置给对应线程。

1
2
3
Thread t = new Thread(new ExceptionThread());
t.setUncaughtExceptionHandler(new MyUnchecckedExceptionhandler());
t.start();

除此之外还有6种方法可以设置,详见链接


[toc]

经典java容器结构图:
image.png

注意哪些属于Collection,哪些属于Map。


List相关问题#

Q:arrList = new ArrayList<>(Arrays.asList()) 和 arrList = Arrays.asList()有什么区别#

1
2
3
4
5
List<Integer> arrList1 = new ArrayList<>(Arrays.asList(1,2,3));
List<Integer> arrList2 = Arrays.asList(1,2,3);

arrList1.add(4);
arrList2.add(4);

A:执行arrList2.add(4)时会报错。
因为 Arrays.asList()只会返回1个固定大小的列表, 其返回的List是AbstractList,无法调用add、remove和clear, 如果调用会直接抛异常。
异常名字为UnsupportedOperationException
具体原因可以见评论区。


Q:那asList()支持通过修改原数组来修改list内容吗?#

例如

1
2
3
4
String[] ss = {"a","b","c"};
List<String> arrList = Arrays.asList(ss);
ss[0] = "d";
System.out.println(arrList);

上面操作后arrList会报错吗?

A:
不会报错。arrList里的内容变成了{“d”,“b”,“c”}

Q: 当输入为哪些字母时,迭代时会报错#

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) throws Exception {
List<String> list = Lists.newArrayList("A", "B", "C", "D");

String s = args[0];
for (String curStr : list) {
if (s.equals(curStr)) {
list.remove(curStr);
}
}
}

A:
删除A或者B会报错, 但是删除C不会!
对于foreach遍历容器,并用remove做删除时,当删除倒数第二个元素时,是不会报错的。
因为:


Q: ArrayList的扩容公式和默认初始大小是多少?#

A:
扩容容量= 原容量 + (原容量右移1位,即0.5倍)= 1.5倍
image.png

初始容量为10.
image.png

  • 注意,初始的数组长度还是0,数组长度和容量不是一个东西。

Q: Vector的扩容公式是多少?#

A:
扩容容量= 原容量 *2
image.png

初始大小为10
image.png


Q:vector和ArrayList的区别#

A:
vector是线程安全的, 每个方法都加了syn关键字,频繁的加锁可能导致性能降低


Q:为什么不推荐使用stack#

A:
为什么不推荐使用stack
https://www.cnblogs.com/cosmos-wong/p/11845934.html
stack 继承自vector , 但是vector里包含了很多不需要的public方法
只是为了实现栈,不用链表来单独实现,而是为了复用简单的方法而迫使它继承 Vector,Stack 和 Vector 本来是毫无关系的。这使得 Stack 在基于数组实现上效率受影响,另外因为继承 Vector 类,Stack 可以复用 Vector 大量方法,这使得 Stack 在设计上不严谨


Map相关问题#

Q: 解答下列特性和哪些Map有关#

  • 实现基于散列表,迭代时是不确定随机的顺序的Map为-----------------------------------------HashMap

  • 迭代时按照插入顺序进行迭代-----------------------------------------------------LinkedHashMap

  • 迭代时按照键值的比较顺序进行迭代-----------------------------------------------------TreeMap

  • 基于红黑树的实现-----------------------------------------------------TreeMap

  • 线程安全的Map-----------------------------------------------------ConcurrentHashMap

  • 键值比较时使用==而不是equal的Map-------------------------------------------------IdentityHashMap

  • 可以按区间得到1个子map的Map----------------------sortMap

  • Set也有HashSet、LinkedSet、TreeSet、SortSet等,作用同理。


Q: hashCode相同, 那么equals肯定true吗?#

A: 不一定。

Q: equals为true, 那么hashCode肯定相同吗?#

A: 对。
原因: 参见散列表的原理,明白hashCode的作用后就明白为什么了。

Q: 2个String如果内容相同,那么hashCode相同吗?#

A: 对,相同。因为二者equals返回true,所以必定hashCode相同。
Q: 如果在插入后,修改某个key的hashCode,可能造成什么问题?
A:
可能造成内存泄漏。因为map是按计算后的hashCode存放的,而如果在外部修改了某个key的值,可能造成之前塞入的那个哈希所在的地址无法被外部remove(key),却又无法被gc(因为一直持有),造成内存泄漏。


HashTable、HashMap、ConcurrentHashMap比较:#

  • key和value不可以为null的是:----------------------------HashTable
  • 线程安全的是:---------------------------------HashTable和ConcurrentHashMap
    他们2个的区别:
    在迭代的过程中,ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map。(具体可参考底层实现原理,总之ConcurrentHashMap最重要的是做了分段)

Q:Collections.synchronizedMap(map)和ConcurrentHashMap,哪个同步效果好?#

A:

Collections.synchronizedMap(map)与ConcurrentHashMap主要区别是:Collections.synchronizedMap()和Hashtable一样,实现上在调用map所有方法时,都对整个map进行同步
而ConcurrentHashMap的实现却更加精细,分端加锁

其实Colletions.synchronizedMap就是对放进去的map包了一层sync关键字。
详见:
https://www.cnblogs.com/a198720/articles/4227500.html


Q:linkedHashMap的accessOrder问题,下面输出什么#

1
2
3
4
5
6
7
8
9
10
11
public void fun2() throws Exception {
LinkedHashMap<String, String> accessOrderTrue = new LinkedHashMap<>(16, 0.75f, true);
accessOrderTrue.put("1","1");
accessOrderTrue.put("2","2");
accessOrderTrue.put("3","3");
accessOrderTrue.put("4","4");
System.out.println("put后的数据:"+accessOrderTrue);
accessOrderTrue.get("2");
accessOrderTrue.get("3");
System.out.println("get后的数据"+accessOrderTrue);
}

A:
省略key值
put后的数据: {1=1,2=2,3=3,4=4}
get后的数据: {1=1,4=4,2=2,3=3}

  • accessOrder为true时, 会把最近访问过的数据放到链表 末尾

Q:hashMap为什么多线程使用时可能会造成死循环?#

A:
https://www.jianshu.com/p/1e9cf0ac07f4
主要发生在2个线程同时put并进行扩容时, 对同一个对象的链表引用会出现问题。、


Q:java1.7 和1.8 之间, hashMap做了什么改进?#

A:
1.7的哈希如果冲突严重,则在一个点上形成的链表会越来越长。
因此1.8做了改进,如果那个点的链表长度超过TREEIFY_THRESHOLD,则会转为红黑树。

Collections#

  • Collection是接口, Collections是1个工具类
  • 排序: Collections.sort(collection ,Comparator<>)
  • 打乱顺序: Collections.shuffle(collection ,Random)
  • 填充: Collections.fill(list, 对象) , 注意是浅拷贝填充, 即填充后使用的是同一个引用。
  • 返回不可变容器(即无法对容器做修改): Collections.unmodifiableMap(容器)
    除此之外可以返回空的不可变集合和仅有单个元素的不可变集合。
    image.png

image.png

特殊的引用容器#

假设有1个对象A, 有1个引用RA指向A

Q:如果RA引用了A,则A必不可能被回收, RA属于什么引用?#

A: 强引用StrongRefernce,默认的引用操作都是强引用


Q: 如果只有RA引用了A, 在内存不足时会强制回收A,则RA属于什么引用?#

A: 软引用SoftReferencem,用法如下:

1
SoftReference<String> softReference = new SoftReference<String>(str);

Q: 如果只有RA引用了A, 不管内存足不足,只要垃圾回收器扫描到了,就会直接回收,RA属于:#

A: 弱引用WeakReference, 注意和垃圾收集器的启动间隔有关,因此短时间内RA还是可用的。
和这个引用相关的集合:WeakHashMap


Q: 当只有RA引用了A时,都会在下一次垃圾收集时直接回收,且创建引用时必须依赖引用队列,这个引用RA属于:#

A: 虚引用(幽灵引用)PhantomReference。
和弱引用区别:
虚引用创建时必须依赖引用队列,且用途一般是跟踪对象的垃圾回收,让用户在回收时通过虚引用去触发一些清理行为。


Q: 引用队列是什么,作用是什么?#

A: 引用队列可以配合软引用、弱引用及幽灵引用使用,当引用的对象将要被JVM回收时,会将其加入到引用队列。
应用:通过引用队列可以了解JVM垃圾回收情况。

1
2
3
4
5
6
7
8
9

// 引用队列
ReferenceQueue<String> rq = new ReferenceQueue<String>();

// 幽灵引用
PhantomReference<String> pr = new PhantomReference<String>(new String(""), rq);

// 永远为null(幽灵引用相当于无引用)
System.out.println(pr.get());

image.png

优先队列#

image.png

[toc]


很多时候提到类加载,大家总是没法马上回忆起顺序,这篇文章会用一个例子为你把类加载的诸多问题一次性澄清。

Java类的加载顺序#

引用1个网上的经典例子,并做稍许改动,以便大家更好地理解。
原例子引用自:https://blog.csdn.net/zfx2013/article/details/89453482

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Animal {
private int i = test();
private static int j ?= method();
static {
System.out.println("a");
}
Animal(){
System.out.println("b");
}
{
System.out.println("c");
}
public int test(){
System.out.println("d");
return 1;
}
public static int method(){
System.out.println("e");
return 1;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Dog extends Animal{
{
System.out.println("h");
}
private int i = test();
static {
System.out.println("f");
}
private static int j ?= method();

Dog(){
System.out.println("g");
}
public int test(){
System.out.println("i");
return 1;
}
public static int method(){
System.out.println("j");
return 1;
}
public static void main(String[] args) {
Dog dog = new Dog();
System.out.println();
Dog dog1 = new Dog();
}
}

执行这段main程序,会输出什么?
答案是
eafjicbhig
icbhig

为了方便大家一个个细节去理解, 我换一种方式去提问。

Q: 什么时候会进行静态变量的赋值和静态代码块的执行?#

A:?

  • 第一次创建某个类或者某个类的子类的实例
  • 访问类的静态变量、调用类的静态方法
  • 使用反射方法forName
  • 调用主类的main方法(本例子的第一次静态初始化其实属于这个情况,调用了Dog的main方法)
    注: 类初始化只会进行一次, 上面任何一种情况触发后,之后都不会再引起类初始化操作。

Q:初始化某个子类时,也会对父类做静态初始化吗?顺序呢?#

A:如果父类之前没有被静态初始化过,那就会进行, 且顺序是先父类再子类。 后面的非静态成员初始化也是如此。
所以会先输出eafj。


Q: 为什么父类的method不会被子类的method重写?#

A: 静态方法是类方法,不会被子类重写。毕竟类方法调用时,是必定带上类名的。


Q: 为什么第一个输出的是e而不是a?#

A: 因为类变量的显示赋值代码和静态代码块代码按照从上到下的顺序执行。
Animal的静态初始化过程中,method的调用在static代码块之前,所以先输出e再输出a。
而Dog的静态初始化过程中,method的调用在static代码块之后,因此先输出f,再输出j


Q: 没有在子类的构造器中调用super()时,也会进行父类对象的实例化吗?#

A: 会的。会自动调用父类的默认构造器。 ?super()主要是用于需要调用父类的特殊构造器的情况。
因此会先进行Animal的对象实例化,再进行Dog的对象实例化


Q: 构造方法、成员显示赋值、非静态代码块(即输出c和h的那2句)的顺序是什么?#

A:?

  1. 成员显示赋值、非静态代码块(按定义顺序)
  2. 构造方法
    因此Animal的实例化过程输出icb(如果对输出i有疑问,见下面一题)
    接着进行Dog的实例化,输出hig

Q: 为什么Animal实例化时, i=test()中输出的是i而不是d?#

A:因为你真正创建的是Dog子类,Dog子类中的test()方法由于签名和父类test方法一致,因此test方法被重写了。?
此时即使在父类中调用,也还是用使用子类Dog的方法。除非你new的是Animal。


Q: 同上题, 如果test方法都是private或者final属性, 那么上题的情况会有变化吗??#

A: ?
因为private和final方法是不能被子类重写的。
所以Animal实例化时,i=test输出d。


总结一下顺序:

  1. 父类静态变量显式赋值、父类静态代码块(按定义顺序)
  2. 子类静态变量显式赋值、子类静态代码块(按定义顺序)
  3. 父类非静态变量显式赋值(父类实例成员变量)、父类非静态代码块(按定义顺序)
  4. 父类构造函数
  5. 子类非静态变量(子类实例成员变量)、子类非静态代码块(按定义顺序)
  6. 子类构造函数。

类加载过程#

Q:类加载的3个必经阶段是:#

A:

  1. 加载(类加载器读取二进制字节流,生成java类对象)
  2. 链接(验证,分配静态域初始零值)
  3. 初始化(前面的题目讲的其实就是初始化时的顺序)
    更详细的如下:
    image.png

被动引用中和类静态初始化的关系#

Q:new某个类的数组时,会引发类初始化吗?#

像下面输出什么

1
2
3
4
5
6
7
8
9
10
11
12
public class Test {
static class A{
public static int a = 1;
static{
System.out.println("initA");
}
}
?
public static void main(String[] args) {
A[] as = new A[5];
}
}

A:?
new数组时,不会引发类初始化。
什么都不输出。


Q:引用类的final静态字段,会引发类初始化吗?#

像下面输出什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
static class A{
public static final int a = 1;
static{
System.out.println("initA");
}
}

public static void main(String[] args) {
System.out.println("A.a=" + A.a);
}
}

A: 不会引发。
不会输出initA。 去掉final就会引发了。
(注意这里必须是基本类型常量, 如果是引用类型产量,则会引发类初始化)


Q:子类引用了父类的静态成员,此时子类会做类初始化嘛?#

如下会输出什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test {
static class A{
public static int a = 1;
static{
System.out.println("initA");
}
}

static class B extends A{
static {
System.out.println("initB");
}
}

public static void main(String[] args) {
System.out.println("B.a=" + B.a);
}
}

A:?
子类不会初始化。
打印initA,却不会打印initB。

类加载器#

双亲委派#


类加载时的双亲委派模型,不知道能怎么出题。。。反正就记得优先去父类加载器中看类是否能加载。
就贴个图吧
image.png


注意,上面的图有问题。
Bootsrap不是ClassLoader的子类,他是C++编写的。
而ExtClassLoader和AppClassLoader都是继承自ClassLoader的

Q:java中, 是否类和接口的包名和名字相同, 那么就一定是同一个类或者接口?#

A:
错误。
1个jvm中, 类和接口的唯一性由 二进制名称以及它的定义类加载器 共同决定。
因此2个不同的加载器加载出来相同的类或接口时, 实际上是不同的。


Q: 讲一下ClassLoader原理, 以及应用场景#

loaderClass 双亲加载实现(这里会体现先去父亲找,再自己)
findClass 如何根据名字,生成1个class(内部需要借助defineClass)
defineClass 通过这个方法生成1个class类

例如需要根据类目,从某个远端网络加载获取这个类, 而且获取过来的时候还是加密的,需要在findClass里对byte数组做解密并加载。

https://blog.csdn.net/zzti_erlie/article/details/82757435


Q:如果在你项目中建一个java.lang.String的类,那系统中用的String类是你定义的String类,还是原生api中的String类?#

A:
用的原生api中的string, 因为双亲委派机制。


Q : 为什么要用双亲委托,有什么好处?#

A:

  • 对于任意一个类,都需要由加载它的"类加载器和这个类本身"来一同确立其在Java虚拟机中的唯一性。那么双亲委派可以保证顺序加载的特性。
  • 核心类的安全。Object类如果不使用双亲委派原则的话,那么A创建的Object对象就可能和B创建的Object是不一样的。不使用双亲委派原则无法保证一些java核心类库的唯一性
  • 例如类java.lang.Object,它存放在rt.jar中,无论哪个类加载器要加载这个类,最终都会委派给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果用户自己写了一个名为java.lang.Object的类,并放在程序的Classpath中,那系统中将会出现多个不同的Object类,java类型体系中最基础的行为也无法保证,应用程序也会变得一片混乱。

Q: 那我如果真的有需求, 不想用双亲的机制呢?#

A:
按照上 面说的, 自己重写类加载的loaderClass就行了, 不走双亲机制的那块代码。


Q: 数组类是怎么做加载的?#

A:
数组类是由AppClassLoader加载的。
数组类打印className时,前面会有个[Lxx类
二维数组就是[[Lxxx类
数组类的父类型是Object

注意此时加载的是数组类,而数组类里面的对象是不会做自动加载的
因此xx类的静态代码并不会被直接调用

[toc]

修饰符#

Q: 各修饰符所代表的可见性?#

public: 可被所有使用
protect: 只能被自己和子类使用,或者同一个包路径
private: 只能自己使用,儿子都不行
不加修饰符即default权限: 包访问权限,和他在同一包内的类都可以访问他,包外的则都不能访问


Q: 外部类可以用private或者protect修饰吗?#

A: 不能,只能用public或者包访问权限。 内部类可以。


解释以下final的作用#

Q: final 成员?#

A: 如果是基本类型,则指值不能被改变。 如果是对象,指对象的引用不可改变,但是引用处的内容可改变。


编译器会要求final成员必须初始化或者构造器里赋值,且后续不能再主动赋值。

Q: final 参数?#

A: 参数不可变,只能读不能修改,同上


Q: final方法#

A: 方法不能被子类重写。


Q: final类#

A: 该类不能被继承。


Q:final局部变量可以作为非final的参数传入吗?会编译报错吗?#

1
2
3
4
5
6
7
public static void main(String[] args){
final A a = new A();
changeA(a);
}
public void changeA(A a) {
// change A...
}

A:
可以作为非final的参数传入,不会编译报错。


#

Q: 重载和重写的区别?#

A:
重载是方法名相同,参数不同。
重写是方法参数等都一致的情况下重写父类的方法。


Q: 如果子类重写了父类中的方法, 那么子类中还能调用父类中的同名方法吗?#

A: 可以,super.xxx即可(C++中不可以调用父类中的同名重载方法)。


Q: 怎样能避免子类在重写父类的方法,不小心弄成了重载?#

(即你想重写父类的f(int), 却不小心写成了f(int,int),导致调用f(int)时还是调用了父类的f ,怎么能避免这种失误?)
A: 加个@Override关键字即可,原文解释:
69e4b13dbfbb57476c7f29e7a5b00ee24db66776


Q:父类的成员变量能被重写/覆盖嘛?#

1
2
3
4
5
6
7
8
9
10
class A{
public String name = "A";
}
class B extends A{
public String name = "B";
}
public static void main{
A a = new B();
System.out.println(a.name);
}

A:
输出A。
注意成员变量不具有多态性,因此你定义的是A,赋值的是B, 那么输出的依旧是A里的成员。
如果是被重写的方法的话,那会用B里的方法。


Q:内部类是啥,内部类能访问外部类的成员吗?#

A:
内部类概念:

1
2
3
4
5
6

class A {
class B{
...
}
}

B就是A的内部类,B能访问A的所有成员


Q: A中有1个内部类C, A的子类B中也有1个内部类C, B中的C会覆盖A中的C吗?#

A: 不会, 因为使用时是通过B.C或者A.C去调用的,存在命名空间的关系。


Q:可以在内部类中定义静态成员吗?#

1
2
3
4
5
6
class A {
class B{
static int b;
...
}
}

A:
不可以。 除非在class B前面加static变为静态类


Q: 匿名类是啥, 匿名类能访问外面的变量或者对象吗?#

A: 匿名类概念:

1
2
3
4
return new A(构造参数){
{构造器内容}
类定义
}

匿名类如果要用外面的对象, 外面的对象必须要定义为final。


Q: 嵌套类是啥,能访问外部类的成员吗?#

A:

1
2
3
4
5
class A {
static int sa;
int a;
static class B{}
}

B只能访问A中的静态成员sa, 而不能访问a。

接口#

类是单继承,接口可以多继承


Q: 接口中如果要定义成员变量,那成员的默认修饰符是什么?#

A: public static final


Q: 接口中各方法的默认修饰符是什么?#

A: public abstract


Q: 接口中可以定义实现具体方法嘛?#

A:
java8以上版本可以。
引入了default关键字,在接口中用default关键字修饰接口,就可以在接口中去实现这个接口了。


枚举#

Q: enum可以被继承吗?#

像下面这样:

1
2
3
enum A extend B{
...
}

A: 不可以。enum标识符本身被编译器处理过,自身就继承自Enum类,而java不支持多重继承。但支持实现接口
6ae62ac7072c2bf308db83b832dafce68ed2d5cf


Q: switch(enum)时需要加default吗?#

A: 可以不需要。


Q: Enum基类里实现了values()方法吗?#

A: 没有实现, values方法是编译器加的。因此从List里取出的对象,是不能调用values()的。
3f7ff65142341ef0771067edc7698616e0ce6154


Q:enum里的枚举的默认修饰符默认是?
A:static final


静态分派和动态分派#

Q: 下面输出什么,属于什么分派?#

1
2
3
4
5
6
7
8
9
10
11
12
void test() {
Father father = new Son(); //静态分派
print(father);
}

void print(Father father) {
System.out.println("this is father");
}

void print(Son son) {
System.out.println("this is son");
}

A:
输出this is father。 属于静态分派。
静态分派概念: 编译器就能确定调用哪个方法。
这里2个print属于重载方法,通过输入参数的定义类型即立刻确定调用哪个
静态分派取决于静态类型

静态类型概念: 编译期写在java文件里能马上看到的类型
例如 A a = Factory.create(args);
那么左边的A就是静态类型, 而右边的类型取决于运行期,是不确定的。


Q: 涉及如下各种不同数据类型的静态分派如何应对?#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Overload {
private static void sayHello(char arg){
System.out.println("hello char");
}
private static void sayHello(Object arg){
System.out.println("hello Object");
}
private static void sayHello(int arg){
System.out.println("hello int");
}
private static void sayHello(long arg){
System.out.println("hello long");
}
// 测试代码
public static void main(String[] args) {
sayHello('a');
}
}

输出什么?
A:
输出 hello char
因为‘a’是一个char类型数据(即静态类型是char),所以会选择参数类型为char的重载方法。
若注释掉sayHello(char arg)方法,那么会输出 hello int
因为‘a’除了可代表字符串,还可代表数字97。因此当没有最合适的sayHello(char arg)方式进行重载时,会选择第二合适(第二优先级)的方法重载,即sayHello(int arg)

总结:当没有最合适的方法进行重载时,会选优先级第二高的的方法进行重载,如此类推。
优先级顺序为:char>int>long>float>double>Character(自动装箱)>Serializable(接口,从下往上)>Object(父类1,父类2,从下往上)>…
其中…为变长参数,将其视为一个数组元素。变长参数的重载优先级最低。
因为 char 转型到 byte 或 short 的过程是不安全的,所以不会选择参数类型为byte 或 short的方法进行重载,故优先级列表里也没有。

上面可以看到,重载时选择方法的优先级顺序是基本类型->高精度类型->包装类->接口(从下往上)->父类(从下往上)->可变参数


Q: 下面输出什么,属于什么分派:#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void test() {
Father father = new Son();
father.name();
}

class Son extends Father {
void name(){
System.out.println("son");
}
}

class Father {
void name(){
System.out.println("father");
}
}

A:
输出son,属于动态分派。运行的时候根据所指向的具体对象才确定调用哪个方法


Q:静态分派属于单分派还是多分派?动态分派属于单分派还是多分派?#

A:
静态分派是多分派。
动态分派是单分派。
多分派概念: 分派时即要考虑调用者的类型,也要考虑参数类型。
而单分派只考虑调用者的类型。

动态分派原理:


Q:类方法在class文件中是什么样的? 是符号引用还是直接引用?#

A:
class文件中, 所定义的方法 都只是符号引用,即只是个符号,知道方法名字, 但是不知道方法的实际指令运行地址
符号引用如下,不过我这展示的时class_info即类的符号引用, 实际上还会有method_info即方法的引用:
ce8d672af3d63cb9a8b56a2325651bea772d8970

然后方法在class文件中时这样存放的, 先是一个method_count数量,接着再存储方法。
03f7962c02a331c88824602f1052a7f866ae687f

此时是不知道方法的指令地址的。 除非从符号引用转为直接引用。


Q:什么时候方法的符号引用会转为实际方法区中的直接引用?#

A:
类加载的解析阶段会把满足「编译期可知,运行期不可变」的方法的符号引用替换为指向方法区的直接引用,不会延迟到运行时再去完成。

  • 构造
  • 私有
  • 静态
    final修饰
    上面这4类方法在类加载时都会被识别出来,并转成 指向方法区的直接引用(即能知道了指令地址了,而不是字节码的符号)

Q:动态分派(即多态), 虚拟机里是怎么确定调用哪个方法的?#

如下, 他怎么确定调用的是Son实现的do, 还是father实现的do?

1
2
3
4

int a = 1;
Father f = new Son()
f.do(a);

A:
首先,通过静态分派, 他知道一定选用的是f(int a) 这个方法,但是他不知道选用哪个类的do(int a)方法。
而你执行f.do(a)时, 操作数栈上会存放一个对象引用

4b25d6e4706cecfa8651680fb19d2603db8113e0

那么执行f方法的虚拟机指令就会通过这个对象引用,找到他的实际类型的class
ee1b41d71b7c0e897c34fa86deb79fb9a242cad8

他会在这个实际类中查找是否有实现这个方法,具体看class文件中有没有这个方法的定义
1dc65dc0cda6cf91e650f456d2351518465e6ebb

如果没有找到,他就去父类找,父类的关系class文件中就可以知道
95427daaed60924141c46d48a56efb4f66c97691

如果父类没有,就接着往上找,直到找到实现了的。

0cd5d2504cda48e554facaded21ffbf82b8af33a

[toc]

Q: break后面加一个label标签是做什么的?像下面这样:#

1
2
3
4
5
6
7
ABC:
while(t++<5){
for(int i=0;i<n;i++){
if(i==1)
break ABC;
}
}

A: break+label标签 是用于从内部退出多层循环的, 上面的例子就是直接从for内部直接退出到while的外面了。


Q: continue后面加一个label标签是做什么的?像下面这样:#

1
2
3
4
5
6
7
ABC:
while(t++<5){
for(int i=0;i<n;i++){
if(i==1)
continue ABC;
}
}

A: 直接contine到ABC的后面,即用于contine到最外层循环, 即走到while(t++<5)那边继续走


Q: switch的default陷阱1,以下输出什么#

1
2
3
4
5
6
7
8
9
10

int i = 0;
switch (i) {
default:
System.out.println("default");
case 0:
System.out.println("0");
case 1:
System.out.println("1");
}

A: 输出"0 1", default都是最后再匹配的。


Q:case后面可以跟变量吗?#

例如

1
2
case a:
case b:

这样子
A:
不可以,case后面只能跟常量。


Q: switch的default陷阱2,以下输出什么#

1
2
3
4
5
6
7
8
9
int i = 3;
switch (i) {
default:
System.out.println("default");
case 0:
System.out.println("0");
case 1:
System.out.println("1");
}

A: 输出"default 0 1", 匹配到default之后,如果没有break还是会一直往下走。


Q: switch() 能识别哪些类型?#

A:
JDK1.0-1.4 数据类型接受 byte short int char
JDK1.5 ? ? ? 数据类型接受 byte short int char enum(枚举)
JDK1.7 ? ? ? 数据类型接受?byte short int char enum(枚举),String 六种类型
PS: 上面提到的基本类型的包装类型也是支持的。


Q: return-finally陷阱1: finally能通过修改变量,来更新return的变量值吗#

1
2
3
4
5
6
7
8
9
int f() {
int a = 1;
try {
return a;
}
finally {
a=2;
}
}

A: 不能, f返回1。
(PS:注意下如果是a引用的话,不能改变返回的a的引用, 但是可以改变a的引用里的属性)
原理见:流程控制语句知识点里的java原理


Q: return-finally陷阱2: finally里也return时,返回哪个?#

1
2
3
4
5
6
7
8
int f() {
try {
return 1;
}
finally {
return 2;
}
}

A:返回finally里的,返回2。


Q: for-each和for-index 哪个快?(就是for(num:nums)和for(int i=0;i<n;i++))#

A: 和场景有关。引用评论区2个小伙伴给的信息:

若实现了RandomAccess接口,那么使用for-index是优于for-each的吧

for-each 比 for-index 快,是不是可以这样考虑:
for-each 是通过内部的迭代器进行遍历的,类似于索引;
for-index 是通过 index 计算偏移量的方式遍历。
—— 这样一来:
对 ArrayList 这样的连续结构来说,for-each 和 for-index 的效率应该不相上下;
而对 LinkedList 这样的链式列表,for-each 的索引优势就体现出来了。


Q: 什么时候没法用for-each代替for-index?#

A: 需要往迭代器中插入元素或者删除元素时。(这会破坏迭代器结构)

[toc]

数据类型#

Q:java中数据类型大小会和平台是32位、64位相关吗?#

A:不相关,虚拟机原因平台兼容


Q:java中解析数据时,需要考虑处理器的大小端问题吗?(即0x1234的12是放在高地址还是低地址)#

A:不需要。java由于虚拟机的关系,屏蔽了大小端问题,需要知道的话可用 ByteOrder.nativeOrder() 查询。在操作ByteBuffer中,也可以使用 ByteBuffer.order() 进行设置:。


Q:java中short、int 、long的字节分别是多少?#

A:2、4、8


Q: float、double是多少字节?#

A:4、8


Q: java中byte、char是多少字节?C++中char是多少字节?#

A : java中是1和2, C++中char是1


Q: java中boolean类型的大小?#

A: bool类型无空间大小(来自java编程思想)
根据http://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html官方文档的描述:

boolean: The boolean data type has only two possible values: true and false. Use this data type for simple flags that track true/false conditions. This data type represents one bit of information, but its “size” isn’t something that’s precisely defined.

布尔类型:布尔数据类型只有两个可能的值:真和假。使用此数据类型为跟踪真/假条件的简单标记。这种数据类型就表示这一点信息,但是它的“大小”并不是精确定义的。

贴一下书中关于布尔数据类型的描述:
img


Q: 不可变类型有哪几种?#

A: short、int、long、float、double、byte、char、boolean的?包装类型, 以及String,这9种属于不可变类型。(这只是部分,还有其他的不可变类)

不可变类型概念:里面的值的内容变了,对应的内存地址也会变化。


Q:类里的成员如果是基本数据类型, 那么会自动初始化吗?初始化成什么?#

A: 会, 初始化为0或者false。


Q: java中局部变量没初始化,会报错吗?#

A: 会


Q: 布尔类型可以强制转化成其他类型吗?#

A : 不能。 boolean b = 1或者boolean b = “true” 是不可以的


Q: 什么时候不能隐式转化?#

A: 如果会丢失精度,则不能隐式转化,比如long转int或者double转long这种。 编译器会强制我们使用强制转化


Q: 8种原始数据类型的类型优先级排序是?#

A:
(byte/short/char)<int<long<float<double
即隐式转换都是从低往高转。


Q:下面哪个是错误的?#

img
A:
B选项是错误的。
因为2个byte类型变量相加的时候,会自动转换成int类型,右边的int类型赋值给short类型便会报错。(好冷的知识)


Q:float f = 1.1;有错吗?#

A:
float浮点后面要加f。加f就代表是float类型,否则就是double类型浮点。

float f = 1.1f;
double d1 = 1.1;


Q: 布尔类型可以做加减乘除吗?#

A : 不能

Q: Integer N = 0; int n = N; 这时候会发生什么?
A: 自动拆包


Q:整型包装类型的比较,下面输出啥?#

Integer num1 = 128,num2 = 128;
System.out.println(num1==num2);

A:
输出false。
值的范围在-128~127的时候Integer可以直接用==比较大小,但是超出这个范围时,==就不管用了,要用equals。
大致原因是在那个范围,Integer的对象会直接用缓存对象,所以地址都相同。
不在那个范围,Integer对象会新生成1个对象,所以地址不同。

另一个注意点: “==” 对于对象来说,比较的是地址。


Q: java中哪2个类可以支持任意精度的整数 和任意精度的浮点数?#

A: BigInteger和BigDecimal

这2个也属于不可变类。


Q: java的数组一定需要我们手动初始化吗?#

A: 不需要,数组元素会自动初始化为null或者0或者false。


Q:java支持C++里的运算符重载吗?#

A: 不支持


Q: if(a=b) 可以吗?#

A: 不行,不能在条件表达式中放入赋值操作。除非a和b都是boolean类型。


Q: 浮点数相等的比较方式#

A:
相等的话要像下面这样

if(Math.abs(a-b))<1E-6F)

如果用浮点的a==b或者a!=b做while循环退出判断,可能会导致死循环


Q: 上面这现象的原因,你了解吗,从浮点数的原理讲一下试试#

A:
3744d5b86b6e1010d046ea4b423e635c5bdc330f

  • 1.1用二进制表示为:1.000110…xxxx…(后面表示省略)
    0.1 = 02(-1)+02(-2)+02(-3)+12(-4)+…
  • 而double类型表示小数部分只有52位,当向后计算 52位后基数还不为0,那后面的部分只能舍弃,从这里可以看出float、double并不能准确表示每一位小数,对于有的小数只能无限趋向它。
  • 在计算机 中加减成除运算实际上最后都要在计算机中转换成二进制的加运算,由此,当计算机运行System.out.println(2.00-1.10);
    时会拿他们在计算机内存中的二进制表示计算,而1.10的二进制表示本身就不准确,所以会出现0.8999999999999999的结果。

Q:下面的数组声明哪几个是对的?#

A. char[] chr1 = new char[]{‘A’,‘B’,‘C’};
B. char[] chr2 = new char[3]{‘A’,‘B’,‘C’};
C. char[][] chr3 = new char[][10];
D. char[][] chr4 = new char[10][];
E. char[] chr5 = new char[3];

A:
ADE是对的。

字符串#


Q: StringBuffer和StringBuilder的区别:#

A:
StringBuffer是线程安全的,但是慢
StringBuilder是线程不安全的(即可以多个线程同时读取他的内容),但是快。


Q:String s = “123”+“456”+“789”;对于这种静态的拼接,用StringBuffer去拼接比用String去拼接要快,对吗?#

A:
错,反编译代码后,我们发现代码是
String s = “123456789”;
因为对于静态字符串的连接操作,Java在编译时会进行彻底的优化,将多个连接操作的字符串在编译时合成一个单独的长字符串。
因此要注意StringBuffer/Builder的适用场合: for循环中大量拼接字符串。
如果是静态的编译器就能感知到的拼接,不要盲目地去使用StirngBuffer/Builder
PS:

如果是字符串变量相加,会优化成StringBuilder做append
如果是常量字符串相加, 则会直接拼接
具体可以查看这篇博文,里面有展示这2 种情况的字节码。
https://blog.csdn.net/weixin_34405557/article/details/89630362?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param


Q:下面9种字符串拼接相等的比较,输出什么结果?为什么?#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hel" + "lo";
String s4 = "Hel" + new String("lo");
String s5 = new String("Hello");
String s6 = s5.intern();
String s7 = "H";
String s8 = "ello";
String s9 = s7 + s8;

System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // true
System.out.println(s1 == s4); // false
System.out.println(s4 == s5); // false
System.out.println(s1 == s6); // true
System.out.println(s1 == s9); // false

A:
println(s1 == s2)输出 true
println(s1 == s3)输出 true
println(s1 == s4)输出 false
println(s4 == s5)输出 false
println(s1 == s6)输出 true
println(s1 == s9)输出 false

字符串的==操作比较的是引用地址。
如果是直接写死在代码里的常量字符串,则地址是固定的,都在常量池中。
写死的常量字符串拼接,依旧会作为常量放入常量池中。(常量池就是指,程序编译的时候,就已经知道了这个字符串)
如果是String类型的,则引用地址是堆中的string对象地址,而非常量池中地址。(因为程序编译的时候,string里的内容不一定是确定的,因此不可能会放到常量池中)
因此涉及string拼接的,都是和原先常量不等。s7和s8已经属于string对象,所以二者不属于常量拼接。
intern会试图把字符串放入常量池。

具体原因可见
https://www.cnblogs.com/syp172654682/p/8082625.html

关于常量池,更多可以见:
原始数据类型强化学习之常量池

可变参数#


Q: 方法重载时,如何选择可变参数和固定参数?像下面输出啥:#

1
2
3
4
5
6
7
8
9
10
 public static void main(String[] args) {
f(1);
}
public static void f(int ...a){
System.out.println("可变参数方法:"+Arrays.toString(a));
}

public static void f(int a){
System.out.println("固定长度 参数方法:"+a);
}

A:
输出固定长度参数方法。
原则:
如果重载方法中,固定参数方法能满足,优先用固定参数方法,不满足时再去选择可变参数方法。


662027daa06620610256ebc9a89705952ca01b0e

参考资料:#

https://www.cnblogs.com/syp172654682/p/8082625.html

[toc]

Executor线程池应用详解#

corePoolSize和maximumPoolSize参数有什么区别?#

A:
当提交新线程到池中时

  • 如果当前线程数 < corePoolSize,则会创建新线程
  • 如果当前线程数=corePoolSize,则新线程被塞进一个队列中等待。
  • 如果队列也被塞满了,那么又会开始新建线程来运行任务,避免任务阻塞或者丢弃
  • 如果队列满了的情况下, 线程总数超过了maxinumPoolSize,那么就抛异常或者阻塞(取决于队列性质)。

  • 调用prestartCoreThread()可提前开启一个空闲的核心线程
  • 调用prestartAllCoreThreads(),可提前创建corePoolSize个核心线程。

keepalive参数是干嘛的?#

A:当线程数量在corePoolSize到maxinumPoolSize之间时, 如果有线程已跑完,且空闲时间超过keepalive时,则会被清除(注意只限于corePoolSize到maxinumPoolsize之间的线程)


核心线程可以被回收吗?(线程池没有被回收的情况下)#

A:
ThreadPoolExecutor有个allowCoreThreadTimeOut(boolean value)方法,可以设置是否在超期后做回收


核心线程数设置多少,怎么考虑?#

A:
io密集型, 可以设置多一点, 因为多一个线程,他可能也没太占cpu,都是在等待IO。
如果是计算密集型,则要设置少一点,别把cpu搞满载了。

有超线程技术的话, 一般可以设置成2倍CPU数量的线程数

超线程技术把多线程处理器内部的两个逻辑内核模拟成两个物理芯片,让单个处理器就能使用线程级的并行计算,进而兼容多线程操作系统和软件。超线程技术充分利用空闲CPU资源,在相同时间内完成更多工作


线程池有哪三种队列策略?#

A:

  1. 握手队列
    相当于不排队的队列。可能造成线程数量无限增长直到超过maxinumPoolSize(相当于corePoolSize没什么用了,只以maxinumPoolSize做上限)
  2. 无界队列
    队列队长无限,即线程数量达到corePoolSize时,后面的线程只会在队列中等待。(相当于maxinumPoolSize没什么用了)
    缺陷: 可能造成队列无限增长以至于OOM
  3. 有界队列

线程池队列已满且maxinumPoolSize已满时,有哪些拒绝策略?#

A:

  • AbortPolicy 默认策略:直接抛出RejectedExecutionException异常
  • DiscardPolicy 丢弃策略: 直接丢了,什么错误也不报
  • DiscardOldestPolicy 丢弃队头策略: 即把最先入队的人从队头扔出去,再尝试让该任务进入队尾(队头任务内心:不公平。。。。)
  • CallerRunsPolicy 调用者处理策略: 交给调用者所在线程自己去跑任务(即谁调用的submit或者execute,他就自己去跑) 注意这个策略会用的比较多
  • 也可以用实现自定义新的RejectedExecutionHandler

线程池为什么需要阻塞队列?#

A:
线程池创建线程需要获取mainlock这个全局锁,影响并发效率,阻塞队列可以很好的缓冲。避免大量线程获取这个创建锁。


五种常见的Executor自带线程池#

有以下五种Executor提供的线程池,注意记忆一下他们的用途,就能理解内部的原理了。

  • newCachedThreadPool: 缓存线程池#

    corePoolSize=0, maxinumPoolSize=+∞,队列长度=0 ,
    因此线程数量会在corePoolSize到maxinumPoolSize之间一直灵活缓存和变动, 且不存在队列等待的情况,一来任务我就创建,用完了会释放。
    image.png

  • newFixedThreadPool :定长线程池#

    corePoolSize= maxinumPoolSize=构造参数值, 队列长度=+∞。
    因此不存在线程不够时扩充的情况

  • newScheduledThreadPool :定时器线程池#

    提交定时任务用的,构造参数里会带定时器的间隔和单位。 其他和FixedThreadPool相同,属于定长线程池。

  • newSingleThreadExecutor : 单线程池#

    corePoolSize=maxinumPoolSize=1, 队列长度=+∞
    只会跑一个任务, 所以其他的任务都会在队列中等待,因此会严格按照FIFO执行

  • newWorkStealingPool(继承自ForkJoinPool ): 工作密取线程池#

如果你的任务执行时间很长,并且里面的任务运行并行跑的,那么他会把你的线程任务再细分到其他的线程来分治。这种特点在于可以在任务队列的两头取任务


submit和execute方法区别是什么?#

A:

  • execute只能接收Runnable类型的任务,而submit除了Runnable,还能接收Callable(Callable类型任务支持返回值)
  • execute方法返回void, submit方法返回FutureTask。
  • 异常方面, submit方法因为返回了futureTask对象,而当进行future.get()时,会把线程中的异常抛出,因此调用者可以方便地处理异常。(如果是execute,只能用内部捕捉或者设置catchHandler)

线程池中, shutdown、 shutdownNow、awaitTermination的区别?#

A:

  • shutdown: 停止接收新任务,等待所有池中已存在任务完成( 包括等待队列中的线程 )。异步方法,即调用后马上返回。
  • shutdownNow: 停止接收新任务,并 停止所有正执行的task,返回还在队列中的task列表 。
  • awaitTermination: 仅仅是一个判断方法,判断当前线程池任务是否全部结束。一般用在shutdown后面,因为shutdown是异步方法,你需要知道什么时候才真正结束。

ForkJoin线程池#

forkJoin核心概念#

ForkJoin线程池在常规的java书籍里还是提到比较少的,毕竟是java8引入的产物。

首先这里简单解释一下forkJoin的运作原理, 本质上有点像归并计算。

  1. 他会将提交大任务按照一定规则拆解(fork)成多个小任务
  2. 当任务小到一定程度时,就会执行计算
  3. 执行完成时会和其他的小任务进行合并(join), 逐步将所有小结果合成一个大结果。
    image.png

可以看这个forkJoinTask的实现伪代码,即如果想使用forkJoin并发执行任务,需要自己把任务继承RecursiveTask,作为forkJoin池的submit对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

public class ForkJoinTask extends RecursiveTask<任务参数> {


public ReckonTask(任务参数) {

}

@Override
protected File compute() {
if(根据任务参数判断任务是否足够小) {
计算,返回
} else {
拆分成子任务1和子任务2
任务1.fork();
任务2.fork();
结果1 = 任务1.join();
结果2 = 任务2.join();
返回结果1+结果2
}
}
}

然后实际上整个forkjoin的细节非常多,这里我通过给自己提好几个问题,来逐步理解forkJoin的原理,


forkJoin中各个线程是如何获取那些小任务的呢?#

A:
他是通过工作密取的方式获取。(java并发那本书里提到过工作密取workSteal,原来是用在这了)

  • 假设我们给forkJoin设置3个工作线程,那么就会有3个工作队列, 注意,这个队列是双端队列。
  • 每当执行任务时,如果不满足小任务的条件,他会fork出2个子任务,并push进自己的工作队列中。
  • 每个工作线程不断取自己队头的任务执行。
  • 关键点:如果自己队列里没有数据,则会从其他队列的队尾取数据。

fork时具体发生了什么?#

A:
是一个异步的操作, 就是向当前线程队列中添加这个fork出来任务,能放进去的话就返回,不会等待。
注意,默认fork出的任务是先默认给自己的。 当自己做不完时,才可能被别人取走!
image.png


join是什么含义?什么时候做的?#

A:
见实现forkJoin任务接口时的代码:
image.png

可以看到时每次fork完之后, 通过join,来获取子task的结果,获取到之后,再合并计算,返回结果。


join这个阻塞过程是怎么做的?如果把线程挂起,那这个线程岂不是无法工作了?#

A:

首先,之前fork时,新的子任务已经被放入队列了。
每个子任务都有一个任务状态。
当调用该子任务的join时, 会循环判断他的状态

如果这个子任务状态未完成, 则从自身队列或其他人的队列中取出新的任务执行,因此进入了下一层的exec()操作。
如果发现子任务状态更新为了完成(这个更新动作可能是自己线程完成的,也可能是别的线程完成的,反正这个任务的状态实现了同步和可见), 则将结果返回给上层。
因此join的本质是一个递归的过程, 任务没完成的话,他就取其他任务继续递归往下执行。

更详细的可以看这个链接fork+join过程详细解读


forkJoin存放任务的时候,怎么保证不会出现并发问题?比如同时往队尾插入的话#

A:

  • n个工作线程是通过数组存放的(即有一个工作线程数组)
  • sun.misc.Unsafe操作类直接基于操作系统控制层在硬件层面上进行原子操作,它是ForkJoinPool高效性能的一大保证,类似的编程思路还体现在java.util.concurrent包中相当规模的类功能实现中。
    image.png

forkJoin应用在哪?#

A:
java8 stream的parallel并发功能就是基于forkJoin做的, parallelStream实现的forkJoin拆解任务和执行任务的接口, 默认用机器所有CPU数量的forkJoin线程池。
如果需要限制线程数量,可以用
new forkJoin(线程数).submit(()->(list.stream().parallel().map()…)); 即可

关于java8和forkJoin究竟是如何配合的,可以看这个链接:
源码级别学习java8并行流执行原理
https://www.cnblogs.com/Dorae/p/7779246.html

TheadLocal核心原理#


ThreadLocal的常见使用场景?#

每个线程中需要维护1个不同的副本, 但这个副本可能是某一个时刻一起塞入每个线程的, 只不过之后该副本的变化 不再受其他线程的影响。

常见场景有连接器管理模块connectorManager, 每个线程持有的connect变量是单独使用的,不会互相影响或者需要加锁。原因就是将其作为副本放入每个线程,当线程启动连接或者关闭时,不影响其他线程里的getConnect方法。


ThreadLocal和Synchronized关键字的区别?#

A:
Synchronized是用时间的消耗,来换取数据同步以及互不冲突
ThreadLocal则是用空间的消耗,来换取数据之间互不冲突(不涉及同步)


TheadLocal在每个线程中是以什么形式存储的? 原理是什么#

这篇文章讲解ThreadLocal源码讲解的蛮好的:
Java并发编程:深入剖析

看完后用我自己的话总结一下就是:

  1. 在某个线程中调用 某threadlocal.set(value)时, 其实就是在该线程中新建了1个threalocalMap, 然后把threadLocal作为键,value作为值,放进本线程的threalocalMap中。

  2. 当在线程中调用threadlocal.get()的时候,就是从线程的threadLocalMap中获取这个threadLocal对应的值
    如果get不到,则可以通过自定义initValue方法生成一个threadLocal的默认值

见如下图所示:
在这里插入图片描述


下面这个代码会报什么错?(例子改编自上面链接的文章)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test {
ThreadLocal<String> stringLocal = new ThreadLocal<String>();

public static void main(String[] args) throws InterruptedException {
final Test test = new Test();

System.out.println(test.getString());

Thread thread1 = new Thread(){
public void run() {
System.out.println(stringLocal.get());
};
};
thread1.start();
thread1.join();
stringLocal.set("thread0")
System.out.println(test.getString());
}
}

在Thread1中,会报空指针, 因为调用get之前没有做过set, 此时做get会报错。
一种方式改成这样:

1
2
3
4
5
6
Thread thread1 = new Thread(){
public void run() {
stringLocal.set("thread1")
System.out.println(stringLocal.get());
};
};

另一种是给stringLocal设置默认值,这种一般用于能直接根据线程推导出初始值的情况:
ThreadLocal stringLocal = new ThreadLocal(){;
protected String initialValue() {
return xxx;
};
};

正确set之后, 答案就会返回thread0和thread1, 且后续怎么set,两边都不会互相影响各自的threadLocal,虽然看起来是都用的是同一个Test里的成员。

1659841108569

本期总结:#

  1. bfs时,必须要记得在对第一个入队节点做vis[start]=true的操作!否则一定会导致WA!

算术三元组的数目 - 力扣 (LeetCode) 竞赛

直接3个for循环搞定


受限条件下可到达节点的数目 - 力扣 (LeetCode) 竞赛

1659841166952

这么简单的bfs题竟然错了一次(用bfs是因为节点数量很多有10^5个)

因为bfs时忘了对第一个入队节点做vis[起点] = true;的操作, 直接导致错失前200

另外这题甚至不应该思考, 直接开写才对

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Solution {
public static List<Integer>[] edgToNextNodeLists(int n, int[][] edges) {
List<Integer>[] list = new List[n];
for (int i = 0;i<n;i++) {
list[i] = new ArrayList<>();
}

for (int[] edg : edges) {
int s = edg[0];
int e = edg[1];
list[s].add(e);
list[e].add(s);
}
return list;
}

public int reachableNodes(int n, int[][] edges, int[] restricted) {
List<Integer>[] nextNodes = edgToNextNodeLists(n, edges);
Queue<Integer> queue = new LinkedList<>();
queue.offer(0);
Set<Integer> resSet = Arrays.stream(restricted).boxed().collect(Collectors.toSet());
boolean[] vis = new boolean[n];
int count = 0;
vis[0] = true;
while (!queue.isEmpty()) {
int node = queue.poll();
count++;
for (int nextNode : nextNodes[node]) {
if (vis[nextNode] || resSet.contains(nextNode)) {
continue;
}
queue.offer(nextNode);
vis[nextNode] = true;
}
}
return count;
}
}

检查数组是否存在有效划分 - 力扣 (LeetCode) 竞赛

1659841394607

可检查范围最多就3个,如果[4,5,6]如果符合,那我只要检查4前面那个数字是否有符合的划分,划分可以传递,因此直接动态规划即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public boolean validPartition(int[] nums) {
int n = nums.length;
boolean[] dp = new boolean[n];
for (int i = 0; i < nums.length;i++) {
int num = nums[i];
if (i-1>=0 && nums[i-1] == num) {
dp[i] |= (i-2>=0?dp[i-2] : true);
}
if (i-2>=0 && nums[i-1] == num && nums[i-2] == num) {
dp[i] |= (i-3>=0?dp[i-3] : true);
}
if (i-2>=0 && nums[i-1] == num-1 && nums[i-2] == num-2) {
dp[i] |= (i-3>=0?dp[i-3] : true);
}
}
return dp[n-1];
}

最长理想子序列 - 力扣 (LeetCode) 竞赛

1659841530495

显然是动态规划, 在[a-z]中找到符合k差值的字母, 根据字母已记录的最长子序列长度,进行+1即可。

最开始想歪了,想成了k是位置距离了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public int longestIdealString(String s, int k) {
int n = s.length();
int[] dp = new int[26];

for (int i = 0; i < s.length();i++) {
int c = s.charAt(i) - 'a';
int max = Integer.MIN_VALUE;
for (int lastc = c-k;lastc <= c+k;lastc++) {
if (lastc < 0 || lastc > 25) {
continue;
}
max = Math.max(max, dp[lastc] + 1);
}
dp[c] = max;
}
return Arrays.stream(dp).max().getAsInt();
}
}