Java要记-持续补充中

1. ArrayList操作自定义对象进行removeAll()时,移除失效原因

由于底层最用调用的是Object的equals()方法进行比较的,比较的是地址,两个对象地址当然是不同的了,移除自然会失败。

解决方案:重写equals方法。【注意重写equals方法记得也要重写hashCode方法】

同时:retainAll()、contains()操作自定义对象时也要注意。

知识点:集合的运算

1. 在进行集合求交集、差集运算时,如果是基本类型的对应的包装类,则可以直接使用交集运算,最终调用的是AbstractCollection中的方法
2. 在比较两个集合是否相同 需要注意`顺序`和`元素个数`
2. 如果集合的泛型是自定义对象时,自定义对象需要重写equals方法
3. 如果集合的泛型是List<T>,且T是自定义对象。需要重写自定义对象的equals方法,还需要对子列表元素按一定规则进行排序。因为最终调用的是AbstractList中的equals方法,该方法的逻辑是既要判断集合长度是否相同,又要判断两个集合中的每个位置上对应的元素是否相同。

案例如下

import java.time.LocalDateTime;
import java.util.Objects;

/**
 * @Description:
 * @Author party-abu
 * @Date 2022/3/16 21:59
 */
public class Stu {

    private String name;

    private Integer age;

    private LocalDateTime createTime;

    public LocalDateTime getCreateTime() {
        return createTime;
    }

    public void setCreateTime(LocalDateTime createTime) {
        this.createTime = createTime;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Stu stu = (Stu) o;
        return Objects.equals(name, stu.name) &&
                Objects.equals(age, stu.age) &&
                Objects.equals(createTime, stu.createTime);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age, createTime);
    }
}
import java.util.*;

/**
 * @Description:
 * @Author party-abu
 * @Date 2021/12/26 20:51
 */
public class TestStream08 {

    public static void main(String[] args) {

        List<List<Stu>> numListOut = new ArrayList<>();

        List<Stu> numListInnerOne = new ArrayList<>();
        Stu stu01 = new Stu();
        stu01.setAge(19);
        stu01.setName("张三");
        numListInnerOne.add(stu01);

        Stu stu02 = new Stu();
        stu02.setAge(22);
        stu02.setName("小明");
        numListInnerOne.add(stu02);

        numListInnerOne.sort(Comparator.comparingInt(Stu::hashCode));

        List<Stu> numListInnerTwo = new ArrayList<>();
        Stu stu03 = new Stu();
        stu03.setAge(20);
        stu03.setName("李四");
        numListInnerTwo.add(stu03);


        numListOut.add(numListInnerOne);
        numListOut.add(numListInnerTwo);

        System.out.println("=======================================");

        List<Stu> numListInnerRequest = new LinkedList<>();
        Stu stu04 = new Stu();
        stu04.setAge(19);
        stu04.setName("张三");

        Stu stu05 = new Stu();
        stu05.setAge(22);
        stu05.setName("小明");

        // 按照顺序添加
        numListInnerRequest.add(stu04);
        numListInnerRequest.add(stu05);
        numListInnerRequest.sort(Comparator.comparingInt(Stu::hashCode));

        List<List<Stu>> listList = Collections.singletonList(numListInnerRequest);

        // 取交集
        numListOut.retainAll(listList);
        System.out.println(numListOut.size() == 1 ? "包含" : "不包含");

    }
}

// 结果是:包含
// 调换位置
numListInnerRequest.add(stu05);
numListInnerRequest.add(stu04);
// 结果是:不包含

2.Java中排序两种常用的是Comparable和Comparator。

根据多个对集合元素进行多级排序使用commons.lang3包下的CompareToBuilder进行比较

Comparable是可以认为是对象的默认排序,Comparator是对象的扩展排序工具

  • 先按age升序,如果相同在按name升序
stu04List.sort(new Comparator<Stu04>() {
    @Override
    public int compare(Stu04 o1, Stu04 o2) {
        return new CompareToBuilder()
            // 现根据age排序,如果age相同,再根据name排序
            .append(o2.getAge(), o1.getAge())
            .append(o1.getName(), o2.getName())
            .toComparison();
    }
});

java8中排序案例,本质还是使用的是Comparator

@Test
public void test13() {

    Stu stu = new Stu();
    stu.setAge(1);
    stu.setName("b");

    Stu stu02 = new Stu();
    stu02.setAge(2);
    stu02.setName("c");

    Stu stu03 = new Stu();
    stu03.setAge(2);
    stu03.setName("a");

    ArrayList<Stu> stuArrayList = new ArrayList<>();
    stuArrayList.add(stu);
    stuArrayList.add(stu02);
    stuArrayList.add(stu03);

    // 多个字段排序
    stuArrayList.stream().sorted((o1, o2) ->
                                 new CompareToBuilder()
                                 .append(o1.getAge(), o2.getAge())
                                 .append(o1.getName(), o2.getName())
                                 .toComparison())
        .forEach(System.out::println);

    // 默认按照名字正序
    stuArrayList.stream().sorted((o1, o2) -> o1.getName().compareTo(o2.getName()))
        .forEach(System.out::println);
    // 默认等价于以下操作
    stuArrayList.stream().sorted(Comparator.comparing(Stu::getName))
        .forEach(System.out::println);

    // 按名字倒序
    stuArrayList.stream().sorted((o1, o2) -> o2.getName().compareTo(o1.getName()))
        .forEach(System.out::println);

}

对中文拼音(常见中文)排序可以使用Collator排序器

ArrayList<String> list = new ArrayList<String>();
    list.add("一鸣惊人-Y");
    list.add("人山人海-R");
    list.add("海阔天空-H");
    list.add("空前绝后-K");
    list.add("后来居上-H");

Collator instance = Collator.getInstance(Locale.CHINA);

list.sort(instance);
list.forEach(System.out::println);

3.jdk1.8 HashMap

HashMap类中有以下主要成员变量:

transient int size;
记录了Map中KV对的个数
loadFactor
装载因子,`用来衡量HashMap满的程度`。loadFactor的默认值为0.75f(static final float DEFAULT_LOAD_FACTOR = 0.75f;)。
int threshold;
临界值,当实际KV个数超过threshold时,HashMap会将容量扩容,threshold=容量*装载因子
除了以上这些重要成员变量外,HashMap中还有一个和他们紧密相关的概念:capacity
容量,如果不指定,默认容量是16(static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;)

image-20220605160003941

put(K key, V value)概要逻辑

public V put(K key, V value) {
    
    return putVal(hash(key), key, value, false, true);
}

static final int hash(Object key) {
    int h;
    // 对key的hashCode做hash运算,如果key是null,那么key是0
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 判断数组Table是否为空,如果是空,则初始化Table数组
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 使用(数组大小-1 位与hash值)定位到在哪一个桶下,如果没有冲突,则将键值对添加进去
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 如果有冲突
    else {
        Node<K,V> e; K k;
        // 如果当前数据的hash值与新添加的hash值相同并且key也相同,就更新键值对
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 如果当前节点是红黑树,则按照红黑树的方式进行添加
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 否则按照链表使用尾插法添加数据
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 判断链表长度是否>=8,如果是,则转为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 当前已存储的数据大小大于了临界值,那么进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
1. 对key的hashCode进行hash运算
2. 判断数组Table是否为空,如果是空,则初始化Table数组
3. 使用(数组大小-1 位与hash值)定位到再在一个桶下
   3.1 如果没有冲突,则将键值对添加进去
   3.2 如果有冲突
  	   3.2.1 如果当前数据的hash值与新添加的hash值相同并且key也相同,就更新键值对
       3.2.2 如果当前节点是红黑树,则按照红黑树的方式进行添加
       3.2.3 否则按照链表使用尾插法添加数据,然后判断链表长度是否>=8,如果是,则转为红黑树
6. 当前已存储的数据大小大于了临界值,那么进行扩容

get(obj)

public V get(Object key) {
    Node<K,V> e;
    // 对key的hashCode进行hash运算
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 使用(数组大小-1 位与hash值)定位到在哪一个桶下
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 如果当前桶第一个节点是否命中,如果命中则返回
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 如果当前桶下一个节点存在
        if ((e = first.next) != null) {
            // 当前节点是红黑树类型,则按照红黑树方法查找
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            // 如果不是红黑树,则做do,while循环定位查找数据
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}
1. 对key的hashCode进行hash运算
2. 使用(数组大小-1 位与hash值)定位到在哪一个桶下
   2.1 如果当前桶第一个节点是否命中,命中则返回第一节点数据
   2.2 如果当前桶下一个节点存在
       3.1 当前节点是红黑树类型,则按照红黑树方法查找
       3.2 否则做do,while循环定位查找数据

4.重写equals与hashcode原因所在

重写equals是为了字面量是否相同

重写hashcode方法后,hashcode是根据类中所有属性值计算的hashCode,保证了在属性值相同的情况下计算对象的hashcode的值也是相同的。如果不重写,那么默认使用的是父类Object的hashCode,该hashCode计算的是对象在内存的地址

在使用Map和Set容器时使用自定义对象作为key,由于数据被存在哪个桶下是通过hash函数的结果得到的,而hash函数是使用key的hashCode计算的。【通常业务上我们认为是如果自定义对象的属性值相同,那么这两个对象就是一样的】倘若自定义对象的hashCode未被重写,那么两个不同对象即使属性值相同,计算得到的hashCode值也是不同的,而hashMap是通过hash函数返回值进行定位的,hashCode不同,hash函数返回值自然也不同,最终导致“相同的自定义对象”多次修改value后,value没有被覆盖,反而每次修改都保存了一份数据,与HashMap键是唯一的特性冲突。

在进行get操作时,是通过key计算先定位到哪个桶中,然后获取到该数据。

5.当集合或者字符串查询为空,应给出空集合[]空字符串“”

6.查询列表应该给出默认排序

7.改动业务逻辑时要考虑先有业务以及影响点【设计优先】

8.自定义异常要重写RuntimeException类的fillInStackTrace方法:

目的是禁止无用且昂贵的堆栈日志追踪,提高效率

@Override
public Throwable fillInStackTrace() {
    return this;
}

9.映射数据库和DTO的属性

不要使用基本基本类型,基本类型都有默认值,会导致将成员变量由于被赋为默认值误用

10.优化查询大量数据:限制每次查询的个数+批处理(比如Mybatis批处理)

11.==和equals的区别

== 是关系运算符,equals() 是方法,结果都返回布尔值

== 作用:
1. 基本类型,比较值是否相等
2. 引用类型,比较内存地址值是否相等

equals()方法的作用:
1. JDK 中的类一般已经重写了 equals(),比较的是内容
2. 自定义类如果没有重写 equals(),将调用父类(默认 Object 类)的 equals() 方法,Object 的 equals() 比较使用了 this == obj

12.同步异步与阻塞非阻塞的主要区别是针对对象不同。

  同步异步是针对调用者来说的,调用者发起一个请求后,一直干等被调用者的反馈就是同步,不必等去做别的事就是异步。

  阻塞非阻塞是针对被调用者来说的,被调用者收到一个请求后,做完请求任务后才给出反馈就是阻塞,收到请求直接给出反馈再去做任务就是非阻塞。

13.Java“值”传递

基本类型:	
    在传值调用中,实际参数先被求值,然后其值通过复制,被传递给被调函数的形式参数。因为形式参数拿到的只是一个"局部拷贝",所以如果在被调函数中改变了形式参数的值,并不会改变实际参数的值。
    
共享对象传递:
    先获取到实际参数的地址,然后将其复制,并把该地址的拷贝传递给被调函数的形式参数。因为参数的地址都指向同一个对象,所以我们称也之为"传共享对象",所以,如果在被调函数中改变了形式参数的值,调用者是可以看到这种变化的。

14.在计算机科学中,使用浮点数来表示实数的近似值

 	单精度浮点型float,用32位存储,1位为符号位, 指数8位, 尾数23位,即:float的精度是23位,能精确表达23位的数,超过就被截取。

	双精度浮点型double,用64位存储,1位符号位,11位指数,52位尾数,即:double的精度是52位,能精确表达52位的数,超过就被截取。

15.字符串String长度限制

字符串有长度限制,在编译期,要求字符串常量池中的常量不能超过65535,并且在javac执行过程中控制了最大值为65534。

在运行期,长度不能超过Int的范围(约21亿,约4g),否则会抛异常。

16.序列化是将对象的状态信息转换为可存储或传输的形式的过程。

在Java中,我们可以通过多种方式来创建对象,并且只要对象没有被回收我们都可以复用该对象。但是,我们创建出来的这些Java对象都是存在于JVM的堆内存中的。只有JVM处于运行状态的时候,这些对象才可能存在。一旦JVM停止运行,这些对象的状态也就随之而丢失了。也就是说java对象数据不是被永久保存着的的,有随时消失的可能性。

但是在真实的应用场景中,我们需要将对象中的数据先保存下来,并且能够在需要的时候把对象重新读取出来。比如在网络传输过程中,如果对象不是持久化状态,那么对象就有随时消失可能性,但这种情况是不允许的。

对象序列化机制是Java语言内建的一种对象持久化方式,通过对象序列化,可以把对象的状态保存为字节数组,并且可以在有需要的时候将这个字节数组通过反序列化的方式再转换成对象。对象序列化可以很容易的在JVM中的活动对象和字节数组(流)之间进行转换。

17.为什么要显式声明SerialVersionUID?

在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException。

当实现java.io.Serializable接口的类没有显式地定义⼀个serialVersionUID变量时候, Java序列化机制会根据编译的Class⾃动⽣成⼀个serialVersionUID作序列化版本⽐较⽤, 这种情况下, 如果Class⽂件没有发⽣变化,就算再编译多次, serialVersionUID也不会变化的。

如果不声明serialVersionUID,在生成字节码会调用computeDefaultSUID()方法计算一个值作为serialVersionUID的值,computeDefaultSUID是通过类名,属性名,属性修饰符,继承的接口,属性类型,名称,方法,静态代码块等等...这些都考虑进去了,都写到一个DataOutputStream中,然后再做hash运算得到一个值,一旦修改了类的东西,计算的serialVersionUID就发生变化了。当反编译的字节流的serialVersionUID与本地实体serialVersionUID不一致时,就会抛出异常InvalidCastException。所以要显式指定serialVersionUID避免这种问题

结论

在序列化过程中,如果被序列化的类中定义了writeObject 和 readObject 方法,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化。

如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。

用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。
1、如果一个类想被序列化,需要实现Serializable接口。否则将抛出NotSerializableException异常,这是因为,在序列化操作过程中会对类型进行检查,要求被序列化的类必须属于Enum、Array和Serializable类型其中的任何一种。

2、在变量声明前加上transient关键字,可以阻止该变量被序列化到文件中。

3、在类中增加writeObject 和 readObject 方法可以实现自定义序列化策略
    
    比如ArrayList中存储的元素elementData使用transient修饰,通过自定义readObject与writeObject方法定制序列化和反序列化,原因如下
    为了防止一个包含大量空对象的数组被序列化,为了优化存储,所以,ArrayList使用transient来声明elementData。但是,作为一个集合,在序列化过程中还必须保证其中的元素可以被持久化下来,所以,通过重写writeObject 和 readObject方法的方式把其中的元素保留下来。

18.静态变量的内存空间是直到程序退出运行,才会释放所有占有的内存

19.数值相加注意达到限制时会变成负数,比如:int 范围【-2147483648---2147483647】

int a = 2147483647;
int b = 1;
System.out.println(a+b);

结果:-2147483648

19.自动拆箱,装箱

自动装箱都是通过包装类的 `valueOf()` 方法来实现的

自动拆箱都是通过包装类对象的 `xxxValue()` 来实现的。

整数类型都有整数池,在-128----127之间使用valueOf()方法都会用到,即进行装箱操作时

20.确定义接口的返回值(boolean/Boolean)类型及命名(success/isSuccess)

应该使用包装类型Boolean以及没有is开头的变量名。原因如下两点

1. 包装类型Boolean:基本类型初始化时有默认值,会导致灵异事件
2. 不同序列化工具对Boolean类型的变量isXX进行序列化、反序列化的`策略是不同的`,在使用一个框架进行序列化,再使用另一个框架进行反序列化时,就很可能导致灵异事件:例1。(比如使用fastJson进行序列化,使用Gson进行反序列化时就会出现问题:如下例子2)
   fastjson、jackson是通过反射遍历出该类中的`所有getter方法`,得到getHollis和isSuccess,然后根据JavaBeans规则,他会认为这是两个属性hollis和success的值,直接序列化成json:{"hollis":"hollischuang","success":true}
   但是Gson并不是这么做的,他是通过反射遍历该类中的所有`属性`,并把其值序列化成json:{"isSuccess":true}

例子1:不同的序列化工具对同一个boolean类型的isSuccess进行序列化

public class BooleanMainTest {

    public static void main(String[] args) throws IOException {
        //定一个Model3类型
        Model3 model3 = new Model3();
        model3.setSuccess(true);

        //使用fastjson(1.2.16)序列化model3成字符串并输出
        System.out.println("Serializable Result With fastjson :" + JSON.toJSONString(model3));

        //使用Gson(2.8.5)序列化model3成字符串并输出
        Gson gson =new Gson();
        System.out.println("Serializable Result With Gson :" +gson.toJson(model3));

        //使用jackson(2.9.7)序列化model3成字符串并输出
        ObjectMapper om = new ObjectMapper();
        System.out.println("Serializable Result With jackson :" +om.writeValueAsString(model3));
    }

}

class Model3 implements Serializable {

    private static final long serialVersionUID = 1836697963736227954L;
    private boolean isSuccess;
    public boolean isSuccess() {
        return isSuccess;
    }
    public void setSuccess(boolean success) {
        isSuccess = success;
    }
    public String getHollis(){
        return "hollischuang";
    }
}

结果如下

Serializable Result With fastjson :{"hollis":"hollischuang","success":true}
Serializable Result With Gson :{"isSuccess":true}
Serializable Result With jackson :{"success":true,"hollis":"hollischuang"}

fastjson、jackson是通过反射读取getXXX方法读取的,认为xXX是属性。Gson是通过反射直接读取属性的,是存在本质的区别


例子2:比如使用fastJson进行序列化,使用Gson进行反序列化时就会出现问题

public class BooleanMainTest {
    public static void main(String[] args) throws IOException {
        Model3 model3 = new Model3();
        model3.setSuccess(true);
        Gson gson =new Gson();
        System.out.println(gson.fromJson(JSON.toJSONString(model3),Model3.class));
    }
}


class Model3 implements Serializable {
    private static final long serialVersionUID = 1836697963736227954L;
    private boolean isSuccess;
    public boolean isSuccess() {
        return isSuccess;
    }
    public void setSuccess(boolean success) {
        isSuccess = success;
    }
    @Override
    public String toString() {
        return new StringJoiner(", ", Model3.class.getSimpleName() + "[", "]")
            .add("isSuccess=" + isSuccess)
            .toString();
    }
}

以上代码,输入结果

以上代码,输出结果:

Model3[isSuccess=false]

明明isSuccess设置的是true,进行反序列化得到的结果是false

21.jdk1.8中StringJoiner

处理字符串拼接指定格式的内容,可以结合stream流处理,底层使用了StringBuilder

22.switch支持byte、short、int、char、枚举、字符串

cast比较本质上:

1. char:对应的ascii码
2. 字符串:对应的hashcode与equals
3. 枚举: 枚举对应的ordinal()方法【每个枚举都对应一个ordinal整数值】

23.字符串常量池、常量池、运行时常量池 区别与理解

  1. 字符串常量

    字符串常量池可以理解为运行时常量池分出来的部分。在加载时,对于class常量池,如果字符串会被装到字符串常量池中。
    
  2. Class常量池

    Class常量池可以理解为是Class文件中的资源仓库。Class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池。
    
    Class常量池包含:字面量和符号引用
    
    作用:
    在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中
    
    也就是说 Class常量池是用来保存常量的一个媒介场所,并且是一个中间场所。在JVM真的运行时,需要把常量池中的常量加载到内存中
    

    字面量只可以右值出现,所谓右值是指等号右边的值,如:int a=123这里的a为左值,123为右值。在这个例子中123就是字面量。

    符号引用是编译原理中的概念,是相对于直接引用来说的。主要包括了以下三类常量:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符

  3. 运行时常量池

    在JVM启动过程中会把Class常量池中的常量加载到内存中,进入到运行时常量池。
    字符串常量池可以理解为运行时常量池分出来的部分。在加载时,对于class常量池,如果字符串会被装到字符串常量池中。
    

24.fail–fast思想【卫语句】

预先识别出一些错误情况,一方面可以避免执行复杂的其他代码,另外一方面,这种异常情况被识别之后也可以针对性的做一些单独处理。

25.java.util.ConcurrentModificationException并非都是并发问题导致的

Iterator类的next()方法

public E next() {
    // fail-fast检验
    checkForComodification();
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}

ArrayList的remove()方法

 public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

案例

List<String> userNames = new ArrayList<String>() {{
    add("Hollis");
    add("hollis");
    add("HollisChuang");
    add("H");
}};

for (String userName : userNames) {
    if (userName.equals("Hollis")) {
        userNames.remove(userName);
    }
}

System.out.println(userNames);

案例反编译后的

public static void main(String[] args) {
    // 使用ImmutableList初始化一个List
    List<String> userNames = new ArrayList<String>() {{
        add("Hollis");
        add("hollis");
        add("HollisChuang");
        add("H");
    }};

    Iterator iterator = userNames.iterator();
    do
    {
        if(!iterator.hasNext())
            break;
        String userName = (String)iterator.next();
        if(userName.equals("Hollis"))
            userNames.remove(userName);
    } while(true);
    System.out.println(userNames);
}

Iterator类的next()方法

public E next() {
    // fail-fast检验
    checkForComodification();
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}


// 如果当前已修改的modCount与期望修改的数量不一致,则抛出异常
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

ArrayList的remove()方法

 public E remove(int index) {
        rangeCheck(index);

        // 每执行一次remove会使modCount+1
        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

结果分析

增强for底层是迭代器,在forEach(迭代器)中使用ArrayList进行remove操作时只会将modCount+1,expectedModCount不做任何处理。这样迭代器在next()获取下一个元素时,会判断expectedModCount与modCount如果不同会抛出并发修改异常

image-20230818162545121
Stu stu = new Stu();
stu.setAge(1);

Stu stu02 = new Stu();
stu02.setAge(2);

ArrayList<Stu> stuArrayList = new ArrayList<>();
stuArrayList.add(stu);
stuArrayList.add(stu02);
stuArrayList.forEach(stuItem -> {
    stuItem.setAge(2);
    System.out.println(stuItem);
});

System.out.println("============");

该例子可以运行,注意:该例子中可以修改集合中的元素本身的内容,而不是集合操作的添加和删除。

《阿里巴巴开发手册》指出禁止在 foreach 循环里进行元素的 remove / add 操作。remove元素请使用 iterator 本身的方式去删除元素。

有以下解决方案

  • 解决方案1:使用迭代器的remove方法
List<String> userNames = new ArrayList<String>() {{
    add("Hollis");
    add("hollis");
    add("HollisChuang");
    add("H");
}};

Iterator<String> iterator = userNames.iterator();
while (iterator.hasNext()){
    if (iterator.next().equals("HollisChuang")){
        iterator.remove();
    }
}

System.out.println(userNames);
  • 解决方案2:使用普通的for循环
List<String> userNames = new ArrayList<String>() {{
    add("Hollis");
    add("hollis");
    add("HollisChuang");
    add("H");
}};

// 注意集合下标变化
for (int i = 0; i < userNames.size(); i++) {
    if (userNames.get(i).equals("HollisChuang")) {
        userNames.remove(i);
    }
}

System.out.println(userNames);
  • 解决方案3:可以使用增强for,但是remove后要执行break跳出循环:原因是remove操作后modCount已经+1了,执行break后不会进入下一次迭代执行next()方法判断modCount是否等于expectModCount了,也就避免了问题
List<String> userNames = new ArrayList<String>() {{
    add("Hollis");
    add("hollis");
    add("HollisChuang");
    add("H");
}};

for (String userName : userNames) {
    if (userName.equals("HollisChuang")) {
        userNames.remove(userName);
        break;
        // remove操作后modCount已经+1了,结束循环,不会进入下一次迭代next进行判断modCount是否等于expectModCount了
    }
}

System.out.println(userNames);

一不小心就踩坑的fail-fast是个什么鬼?-HollisChuang's Blog

26.类的初始化是延迟的

直到类第一次被主动使用时JVM才会执行clinit方法【执行静态代码块以及静态变量的赋值动作】,且只执行一次clinit

27.枚举类天生就是线程安全的原因:

当一个Java类第一次被真正使用到的时候静态资源被初始化,Java类的加载和初始化过程都是线程安全的(因为枚举常量都是静态的,虚拟机在加载枚举的类的时候,会使用ClassLoader的loadClass方法,而这个方法使用同步代码块保证了线程安全)。所以,创建一个enum类型是线程安全的。
  • 单例模式最好的实现方式是—枚举

    • 写法简单

    • 天生就是线程安全——类加载过程就保证了枚举常量的线程安全

    • 可以防止序列化——编译器禁止对readObject()与writeObject进行定制序列化的

      • 反射破坏单例——枚举常量是默认静态的,类加载时只会初始化一次,反射是无法创建新实例的

      • 双重检索实现单例模式——对序列化和反射进行了限制

        import java.io.Serializable;
        
        /**
         * @author party-abu
         */
        public class SafeDclSingleton implements Serializable {
        
            private static final long serialVersionUID = 1L;
            /**
             * 添加一个标识符flag。注意修饰符要为static,目的是当flag被修改,其他对象立即可见
             */
            private static boolean flag = false;
        
            /**
             * 假如场景如下:
             * 线程A先来的,第一次执行构造函数创建对象时,发现flag=false,构造器将flag改为true,
             * 这时线程B过来想尝试执行构造函数创建新对象,发现flag=true,就会直接抛出异常,
             * 这样保证了DCL下单例不会被破坏。
             */
            private SafeDclSingleton() {
                if (!flag) {
                    flag = true;
                } else {
                    throw new RuntimeException("使用反射破解反射是没有用的");
                }
            }
        
            private static volatile SafeDclSingleton dclSingleton = null;
        
            public static SafeDclSingleton getDclSingleton() {
                if (dclSingleton == null) {
                    synchronized (SafeDclSingleton.class) {
                        if (dclSingleton == null) {
                            dclSingleton = new SafeDclSingleton();
                        }
                    }
                }
                return dclSingleton;
            }
        
        
            /**
             * 
             * 防止序列化导致单例被破坏
             * @return
             */
            private Object readResolve() {
                return dclSingleton;
            }
        }
        
        • 使用反射尝试破坏单例
        import java.lang.reflect.Constructor;
        
        /**
         * @description:
         * @author: party-abu
         * @create: 2022-06-04 11:34
         */
        public class ReflectAttackSingle {
        
            public static void main(String[] args) throws Exception {
                // 获取Class对象
                Class<SafeDclSingleton> slc = SafeDclSingleton.class;
                Constructor<SafeDclSingleton> slcConstructor = slc.getDeclaredConstructor();
                // 忽略权限修饰符
                slcConstructor.setAccessible(true);
                // 尝试构建第一个对象
                SafeDclSingleton obj01 = slcConstructor.newInstance();
                SafeDclSingleton obj02 = slcConstructor.newInstance();
                System.out.println(obj01);
                System.out.println(obj02);
                System.out.println(obj01 == obj02);
            }
        }
        
        • 序列化后尝试进行破坏单例模式
        /**
         * Created by hollis on 16/2/5.
         */
        public class SerializableDemo {
            //为了便于理解,忽略关闭流操作及删除文件操作。真正编码时千万不要忘记
            //Exception直接抛出
            public static void main(String[] args) throws IOException, ClassNotFoundException {
                //Write Obj to file
                ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
                oos.writeObject(SafeDclSingleton.getDclSingleton());
                //Read Obj from file
                File file = new File("tempFile");
                ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
                SafeDclSingleton newInstance = (SafeDclSingleton) ois.readObject();
                //判断是否是同一个对象
                System.out.println(newInstance == SafeDclSingleton.getDclSingleton());
            }
        }
        

        得出结论:单例模式最好的实现方式是——枚举深入分析Java的序列化与反序列化-HollisChuang's Blog

        单例与序列化的那些事儿-HollisChuang's Blog

28.CopyOnWriteArrayList

使用读写分离的思想:读的时候是原容器,写的时候是将原容器赋值一份,然后在新容器里进行写,写完之后会将原容器的引用指向新容器里。当前读的数据不一定是最新的,因为写时复制的思想是通过延迟更新的策略实现最终一致的。

29.动态代理

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * @author party-abu
 */
public class MyInvocationHandler implements InvocationHandler {

    private Object target;

    public MyInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("before");
        Object result = method.invoke(target, args);
        System.out.println("after");
        return result;
    }

    public Object getProxy() {

        return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), target.getClass().getInterfaces(), this);

    }

    public static void main(String[] args) {

        UserService service = new UserServiceImpl();
        MyInvocationHandler handler = new MyInvocationHandler(service);
        UserService proxy = (UserService) handler.getProxy();
        proxy.add();
    }
}

30.注解

  • 元注解

    • @Target:在哪里使用
    • @Retention:在什么级别下保留该注解
    • @Inherited:允许子类继承父类注解
    • @Documented:将此注解包含在javadoc中
  • @Deprecated 表示方法已经过时,方法上有横线,使用时会有警告。

  • @SuppressWarnings 表示关闭一些警告信息(通知java编译器忽略特定的编译警告)

  • @FunctionalInterface (jdk1.8更新) 表示:用来指定某个接口必须是函数式接口,否则就会编译出错。

31.异常

在不进行对异常进行任何处理时,程序一旦遇到异常就会在发生异常的位置中断程序的执行,原因是异常处理器进行了捕获并抛出了异常
image-20220731184537043
  • 异常被往上抛出,则抛出异常之后的代码将不被执行。
  • 异常被处理,并且异常处理逻辑没有结束或者抛出异常,则之后的代码都可以正常执行。

正确处理异常的步骤

处理异常千万什么也不做,仅仅是e.printStacktrace()。如果知道怎么处理,就处理掉,不知道怎么处理就往上抛,交给调用者去处理

32.时间

时间戳:是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数

一般使用GMT+8表示中国的时间,是因为中国位于东八区,时间上比格林威治时间快8个小时。

33.URL编解码

网络标准RFC 1738做了硬性规定 :

只有字母和数字[0-9a-zA-Z]、一些特殊符号“$-_.+!*'(),”[不包括双引号]、以及某些保留字,才可以不经过编码直接用于URL;除此以外的字符是无法在URL中展示的,所以,遇到这种字符,如中文,就需要进行编码。

所以,把带有特殊字符的URL转成可以显示的URL过程,称之为URL编码。

反之,就是解码。

URL编码可以使用不同的方式,如escape,URLEncode,encodeURIComponent。

34.增强for底层是普通for、迭代器

使用增强for遍历数组,底层是普通for;遍历集合,底层是迭代器

35.Lambda

1. 方法的参数或返回值必须是函数式接口,才能使用lambda作为方法的实例
2. Labmda表达式是一个的语法糖,但不是匿名内部类的语法糖。实现方式其实是依赖了几个JVM底层提供的lambda相关api。

Java中语法糖原理、解语法糖 (hollischuang.github.io)

36.BigDecimal实现

通过`无标度值`和`标度`来表示的

scale作用:

如果scale为零或正值,则该值表示这个数字小数点右侧的位数。如果scale为负数,则该数字的真实值需要乘以10的该负数的绝对值的幂。例如,scale为-3,则这个数需要乘1000,即在末尾有3个0。

如:

  • 123.123,那么如果使用BigDecimal表示,那么他的无标度值为123123,他的标度为3。

  • new BigDecimal("0.10000")和new BigDecimal("0.1")这两个数的标度分别是5和1,如果使用BigDecimal的equals方法比较,得到的结果是false

37.try-with-resource语法糖

从jdk1.7开始,如果实现了Closeable接口的资源,使用try,catch后可以自动关闭

import org.junit.Test;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class Demo05 {

    @Test
    public void test14() {
        try (BufferedReader br = new BufferedReader(new FileReader("C:\\Users\\86167\\Desktop\\学习之道.md"))) {
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            // handle exception
        }
    }
}

反编译后

import java.io.*;

public class Demo05
{

    public Demo05()
    {
    }

    public void test14()
    {
        BufferedReader br;
        Throwable throwable;
        br = new BufferedReader(new FileReader("C:\\Users\\86167\\Desktop\\\u5B66\u4E60\u4E4B\u9053.md"));
        throwable = null;
        String line;
        try
        {
            while((line = br.readLine()) != null) 
                System.out.println(line);
        }
        catch(Throwable throwable2)
        {
            throwable = throwable2;
            throw throwable2;
        }
        // 关闭资源
        if(br != null)
            if(throwable != null)
                try
                {
                    br.close();
                }
                catch(Throwable throwable1)
                {
                    throwable.addSuppressed(throwable1);
                }
            else
                br.close();
        break MISSING_BLOCK_LABEL_113;
        Exception exception;
        exception;
        if(br != null)
            if(throwable != null)
                try
                {
                    br.close();
                }
                catch(Throwable throwable3)
                {
                    throwable.addSuppressed(throwable3);
                }
            else
                br.close();
        throw exception;
        IOException ioexception;
        ioexception;
    }
}

38.注意ArrayList.subList()方法

获取的数据是一个SubList类的对象,该对象只是截取了当前ArrayList片段,只是一个视图,对SubList对象进行修改同样也会影响到原来的ArrayList对象中的数据。同时使用增强for对subList对象遍历remove、add操作,同样也会导致ConcurrentModificationException异常

ArrayList<String> schoolNumList = new ArrayList<>();
schoolNumList.add("东校区");
schoolNumList.add("西校区");
schoolNumList.add("南校区");
List<String> subList = schoolNumList.subList(0, 3);
for (String element : subList) {
    if ("东校区".equals(element)) {
        subList.remove(element);
    }
}

39.LinkedList为什么在删除和新增时建议使用呢?查找、修改元素为什么建议使用ArrayList呢?

1. LinkedList基于双向链表。插入与删除效率快。原因是ArrayList每次在指定位置插入时,插入后面的元素都要往后边移动一个位置,每次在指定位置删除时,插入后面的元素都要往前边移动一个位置,每次都要copy,是一个消耗资源的过程。而LinkedList只是前后指针的变更,没有任何很耗时的地方。

2. 使用ArrayList基于数组。进行查找、修改是通过下标获取元素修改的,而LinkedList是通过遍历整个集合找到相应的元素的。相比之下,通过下标获取元素更快。

40.为什么java中调用对象会自动调用对象的toString方法呢?

// System.out.println(stu); println方法底层如下

public void println(Object x) {
    // 调用String.valueOf()方法
    String s = String.valueOf(x);
    synchronized (this) {
        print(s);
        newLine();
    }
}

// 如果obj是null,那么返回null字符串,如果不是则调用对象的toString()方法
public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();
}

41.TreeSet、HashSet、LinkedHashSet区别?

相同点:
都能保证元素唯一性

不同点:
1. TreeSet实现了SortSet,默认是自然排序的,当然也可以定制排序规则,适用于一开始添加后元素,元素不变的集合。因为一旦改变TreeSet中的元素,顺序就不会自动排序了
【非稳定排序使用List,使用Collections.sort(list),如果要去重可以使用new HashSet<>(list);转为HashSet
2. LinkedHashSet在取得元素顺序是按照添加元素顺序来取
3. HashSet取数据是无序的

42.自定义注解+枚举使用更加强大

43.当前线程遇到异常时,线程池会怎么做?

一个线程出现异常不会影响线程池里面其他线程的正常执行
线程不是被回收而是线程池把这个线程移除掉,同时创建一个新的线程放到线程池中。

44.catch异常,打印具体exception

反例:

try{
  // do something
}catch(Exception e){
  log.info("捡田螺的小男孩,你的程序有异常啦");
}

正例:

try{
  // do something
}catch(Exception e){
  log.info("捡田螺的小男孩,你的程序有异常啦:",e); //把exception打印出来
}

理由:反例中,并没有把exception出来,到时候排查问题就不好查了啦,到底是SQl写错的异常还是IO异常,还是其他呢?所以应该把exception打印到日志中哦~


  • 尽量不要使用e.printStackTrace()打印,可能导致字符串常量池内存空间占满

  • catch了异常,使用log把它打印出来

  • 不要用一个Exception捕捉所有可能的异常

  • 不要把捕获异常当做业务逻辑来处理

45.Array.asList

  • Arrays.asList(T)泛型必须是包装类型
int[] array = {1, 2, 3};
List list = Arrays.asList(array);
System.out.println(list.size());

1

46. 调用第三方接口,需要考虑异常处理,安全性,超时重试这几个点。

日常开发中,经常需要调用第三方服务,或者分布式远程服务的的话,需要考虑:

  • 异常处理(比如,你调别人的接口,如果异常了,怎么处理,是重试还是当做失败)
  • 超时(没法预估对方接口一般多久返回,一般设置个超时断开时间,以保护你的接口)
  • 重试次数(你的接口调失败,需不需要重试,需要站在业务上角度思考这个问题)

简单一个例子,你一个http请求调别人的服务,需要考虑设置connect-time,和retry次数。

47.集合判断是否为空?

1. 集合本身是不是为null:ArrayList list == null ?
2. 集合元素个数是否等于0
3. 遍历集合时,要确定集合中每个元素都是非空情况下才能进行遍历获取元素,否则容易出现空指针异常

48.@Transactional()

对于@Transactional 注解,当 spring 遇到该注解时,会自动从数据库连接池中获取 connection,并开启事务然后绑定到 ThreadLocal 上,如果业务并没有进入到最终的 操作数据库环节,那么就没有必要获取连接并开启事务,应该直接将 connection 返回给数据库连接池,供其他使用。

注意:使用编程式事务的好处就是可以精细化控制事务范围

如果一个事务只有多个查询语句,也要开启事务,并将readOnly设置ture,好处是:

  1. Spring可以在运行期对其优化
  2. 保证读到一致性的数据。因为如果不开启事务,在多次查询同一个数据时,有其他事务对其修改,那么中间读到的数据是脏数据,开启了事务之后,Innodb引擎下使用可重读隔离级别可以保证防止出现不可重复读,幻读等问题

使用事务时尽量使用编程式事务,避免将耗时操作(包括远程调用)放入事务中

49.防止空指针和下标越界

这是我最不喜欢看到的异常,尤其在核心框架中,我更愿看到信息详细的参数不合法异常。这也是一个编写健壮程序的开发人员,在写每一行代码都应在潜意识中防止的异常。基本上要能确保每一次写完的代码,在不测试的情况下,都不会出现这两个异常才算合格。

50为什么选择抛异常而不是直接return?

1. 事务原因,事务只有在出现异常才会回滚
2. 面向对象编程,抛出的异常信息本质上会被封装成一个异常对象,然后会调用Throwable构造方法,构造方法中含有fillInStackTrace();方法,打印堆栈信息

51.数组越界导致IndexOutOfBoundException

String name = list.get(1).getName(); //list可能越界,因为不一定有2个元素哈

52.使用集合自带的方法处理交集、并集、差集,无重复并集

原因:

1. 优雅
2. 效率高,底层使用的是System.arrayCopy()方法

无重复并集:保留了两个集合中交集中的一份

list1:[1,1,3,4]
list2:[1,1,8,9]

list1.removeAll(list2);
list1.addAll(list2);

list1 无重复交集最终结果:[1,1,3,4,8,9]
list1 与 list2 去重后的最终结果:[1,3,4,8,9]

注意要和两个集合去重要分清楚:
无重复交集只保留了 两个集合的结果只有交集中的一份
去重则是两个集合中的所有元素都是唯一的

53.return、contine、break、抛异常

退出方法两种方式:

异常调用完成、正常调用完成
正常调用完成:1.return 结束当前方法、2.return value 结束当前方法并返回一个值
异常调用完成:方法再执行过程中遇到了异常,并且这个异常没有在方法体内得到妥善处理,就会导致方法退出

异常调用:退出方式是在方法执行的过程中遇到了异常,并且这个异常没有在方法体内得到妥善处
理。无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方
法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为“异常调用
完成


contine:结束当前循环,继续执行下次循环
break:跳出循环

Java编程思想一书中,对异常的总结。

  1. 在恰当的级别处理问题。(在知道该如何处理的情况下了捕获异常。)
  2. 进行少许修补,然后绕过异常发生的地方继续执行。
  3. 终止程序。

54.查看控制台异常信息

从上往下看日志,因为程序遇到异常的地方最先被JVM输出打印
public class PrintStackTraceTest {

    public static void main(String[] args) {
        firstMethod();
    }

    public static void firstMethod() {
        secondMethod();
    }

    public static void secondMethod() {
        thirdMethod();
    }

    public static void thirdMethod() {
        throw new SelfException("自定义异常信息");
    }
}

class SelfException extends RuntimeException {
    SelfException() {
    }

    SelfException(String msg) {
        super(msg);
    }
}
Exception in thread "main" com.abu.demo01.SelfException: 自定义异常信息
	at com.abu.demo01.PrintStackTraceTest.thirdMethod(PrintStackTraceTest.java:18)
	at com.abu.demo01.PrintStackTraceTest.secondMethod(PrintStackTraceTest.java:14)
	at com.abu.demo01.PrintStackTraceTest.firstMethod(PrintStackTraceTest.java:10)
	at com.abu.demo01.PrintStackTraceTest.main(PrintStackTraceTest.java:6)

总结

  1. 异常从发生异常的方法逐渐向外传播,最先打印的是发生异常的根本地方。首先传给该方法的调用者,该方法调用者再次传给其调用者……,直至最后传到 main 方法,如果 main 方法依然没有处理该异常,则 JVM 会中止该程序,并打印异常的跟踪栈信息。

  2. 如果该异常没有得到处理,将会导致该线程中止运行

Java的异常跟踪栈

55.oop思想

OOP 其实就是一种代码封装思想,它将相关变量和函数放到一个类中,并将它们保护和隔离起来,形成一个一个的小模块,每个小模块能够完成一个小任务。

56.增强for还是forEach

增强for、jdk8的forEach都不能再遍历时又调用remove()方法,只能对元素进行修改,不能对集合元素增加、删除,否则抛ConcurrentModificationException异常

57.使用stream流

调用stream()时要注意数组或集合是否为null,如果为null,会抛出NPE
public static <T> Stream<T> stream(Spliterator<T> spliterator, boolean parallel) {
    Objects.requireNonNull(spliterator);
    return new ReferencePipeline.Head<>(spliterator,
                                        StreamOpFlag.fromCharacteristics(spliterator),
                                        parallel);
}


public static <T> T requireNonNull(T obj) {
    if (obj == null)
        throw new NullPointerException();
    return obj;
}

58.JVM处理异常机制流程

public static void simpleTryCatch() {
   try {
       testNPE();
   } catch (Exception e) {
       e.printStackTrace();
   }
}
//javap -c Main
 public static void simpleTryCatch();
    Code:
       0: invokestatic  #3                  // Method testNPE:()V
       3: goto          11
       6: astore_0
       7: aload_0
       8: invokevirtual #5                  // Method java/lang/Exception.printStackTrace:()V
      11: return
    Exception table:
       from    to  target type
           0     3     6   Class java/lang/Exception
1.JVM会在当前出现异常的方法中,查找异常表,是否有合适的处理者来处理 
2.如果当前方法异常表不为空,并且异常符合处理者的from和to节点,并且type也匹配,则JVM调用位于target的调用者来处理。
3.如果上一条未找到合理的处理者,则继续查找异常表中的剩余条目 
4.如果当前方法的异常表无法处理,则向上查找(弹栈处理)刚刚调用该方法的调用处,并重复上面的操作。 
5.如果所有的栈帧被弹出,仍然没有处理,则抛给当前的Thread,Thread则会终止。 
6.如果当前Thread为最后一个非守护线程,且未处理异常,则会导致JVM终止运行。

59.默认赋值不复制问题

静态变量、成员变量:有初始化赋零值阶段,可以直接使用
局部变量、常量(final修饰的):没有初始化赋零值阶段,必须显式赋值才能使用

60.为什么Tomcat的类加载器也不是双亲委派模型

我们知道,Java默认的类加载机制是通过双亲委派模型来实现的,而Tomcat实现的方式又和双亲委派模型有所区别。

原因在于一个Tomcat容器允许同时运行多个Web程序,每个Web程序依赖的类又必须是相互隔离的。因此,如果Tomcat使用双亲委派模式来加载类的话,将导致Web程序依赖的类变为共享的。

举个例子,假如我们有两个Web程序,一个依赖A库的1.0版本,另一个依赖A库的2.0版本,他们都使用了类xxx.xx.Clazz,其实现的逻辑因类库版本的不同而结构完全不同。那么这两个Web程序的其中一个必然因为加载的Clazz不是所使用的Clazz而出现问题!而这对于开发来说是非常致命的!

61.接口幂等和防止重复提交是一回事吗

严格来说,并不是。

  1. 幂等: 更多的是在重复请求已经发生,或是无法避免的情况下,采取一定的技术手段让这些重复请求不给系统带来副作用。
  2. 防止重复: 提交更多的是不让用户发起多次一样的请求。比如说用户在线购物下单时点了提交订单按钮,但是由于网络原因响应很慢,此时用户比较心急多次点击了订单提交按钮。 这种情况下就可能会造成多次下单。一般防止重复提交的方案有:将订单按钮置灰,跳转到结果页等。主要还是从客户端的角度来解决这个问题。

62.Thread.join()

本质上是调用Thread.wait()使获取锁的main线程阻塞;先让当前线程先执行完,然后唤醒main线程。

Thread.wait():让线程处于等待,并释放掉锁。等待其他线程调用notify,notifyAll唤醒

Thread.sleep(时间):让线程处于阻塞指定时间,时间结束会继续运行,不会释放锁。

 public static void main(String[] args) throws InterruptedException {

        Thread threadOne = new Thread(() -> {
            System.out.println("子线程1");
        });

        Thread threadTwo = new Thread(() -> {
            System.out.println("子线程2");
        });

        threadOne.start();
        threadTwo.start();

        // 使获取锁的main线程阻塞;先让threadOne先执行完,然后唤醒main线程
        threadOne.join();
        // 使获取锁的main线程阻塞;先让threadTwo先执行完,然后唤醒main线程
        threadTwo.join();

        System.out.println("main");
    }

63.线程池为什么使用阻塞队列存放任务呢?

1. 自带阻塞、唤醒机制,实现简单
2. 没有元素时,take操作的线程被阻塞;元素满后,put操作的线程被阻塞,线程释放了cpu资源

64.SPI:服务发现提供机制

是jdk内置的,可以为接口自动查找到实现类的机制,用来替换或扩展组件

65.final修饰变量和引用

final关键字修饰的成员变量如果是是引用类型的话,则表示这个引用的地址值是不能改变的,但是这个引用所指向的对象里面的内容还是可以改变的

66.hashMap与ConcurrentHashMap

内容1:
当链表长度>=8,并且当数组长度小于64 开始扩容  
与
链表长度>=8 并且数组长度大于64才会树化操作   
并不是if,else关系,它是if,else if关系。

也就是说当链表长度大于等于8时,树化操作一定会执行的。

---------------------------------------------
内容2:HashMap添加元素时,旧值覆盖是在以下逻辑进行的
    if (e != null) { // existing mapping for key
        V oldValue = e.value;
        if (!onlyIfAbsent || oldValue == null)
            e.value = value;
        afterNodeAccess(e);
        return oldValue;
    }

-----------------------------------------------
内容3:ConcurrentHashMap采用的是CAS自旋+synchronize锁

67.与、或、异或

与运算(&):两个都是1,结果才是1

或运算(|):只要有一个是1,结果就是1

异或运算(^):两个数不同就是1,否则是0

68.for循环进行return

1. 普通的for循环或增强for进行return,跟Java中原生的return含义一样,即终止当前方法执行
2. 1.8中forEach进行return时,含义有所改变,跟continue含义一样,结束当前循环,继续执行下次循环
List<Integer> list = Arrays.asList(1, 2, 3, 4);
for (Integer element : list) {
    if (element % 2 == 0) {
        return;
    } else {
        System.out.println(element + "为奇数");
    }
}
System.out.println("main");

输出:

1为奇数

List<Integer> list = Arrays.asList(1, 2, 3, 4);
list.forEach(item -> {
    if (item % 2 == 0) {
        return;
    }
    System.out.println(item + "为奇数");
});

System.out.println("main");

输出:

1为奇数
3为奇数
main

69.自测BCDE原则

image-20230902144216587

70.finally中使用return

finally中添加return会导致try中与catchreturn**失效

71.try-with-resouces

需要手动关闭流

try{
FileInputStream fi = new FileInputStream();
//逻辑
}catch(Exception ex){

}

使用try-with-resouces后

try(FileInputStream fi = new FileInputStream()){
// 逻辑
}catch(Exception ex){

}

72. if else与switch相同点

if()else{} if()else{} else{}
与switch都共同特点是只会对一个选择项执行

73.元素为0集合遍历方式

  1. 集合不为null,但元素为0时,使用stream流或者增强for都会跳过方法体执行,不会有异常
  • stream流遍历集合时,当发现元素为0,不会执行任何操作,直接返回空集合
  • 使用增强for遍历集合时,本质上使用的是迭代器,迭代器会判断是否有下一个元素,如果有的话才会执行方法体操作
  1. 集合不为null,但元素存在null值,stream流和增强for遍历使用到元素计算时会出现NPE
List<Integer> numList = Arrays.asList();

1.使用集合操作
for (Integer num : numList) {
 num++;
 System.out.println("num = " + num);
}

2.使用stream流操作时
numList.stream().map(t -> t + 1).forEach(System.out::println);

74.针对Boolean类型的不同序列化工具影响

public class BooleanMainTest {
    public static void main(String[] args) throws IOException {
        Model3 model3 = new Model3();
        model3.setSuccess(true);
        Gson gson =new Gson();
        // 使用fastjson序列化,gson反序列化导致boolean类型值不符合预期
        // 原因是fastjson使用的getXXX方法拿到的序列对象,gson使用属性名进行序列化的
        System.out.println(gson.fromJson(JSON.toJSONString(model3),Model3.class));
    }
}

针对Boolean类型,应该使用不用is开头的属性名

75.为什么关闭IO流

JAVA垃圾回收器有两个显著的特点:

1. gc 只能释放内存资源,而不能释放与内存无关的资源
2. gc 回收具有不确定性,也就是说你根本不知道它什么时候会回收

凡跨出Java虚拟机的资源都要自己手动操作关闭,因为这些资源不受Java虚拟机的限制

76.慎用CopyOnWriteArrayList

适用于读多写少场景。写多读少使用Collections.synchronizedList
1. 添加时会copy原容器内容,量大时可能会导致fullGC频繁,影响性能
2. 读操作读的可能是没及时更新的旧数据

77.关于抛异常与返回结果集

1. 单个内部服务使用抛异常,包裹成服务内部自定义异常
2. RPC/http等跨服务间调用使用状态码

理由如下:

在代码中使用“抛异常”还是“返回错误码”,对于公司外的 http/api 开放接口必须 使用“错误码”;而应用内部推荐异常抛出;跨应用间 RPC 调用优先考虑使用 Result 方式,封 装 isSuccess、“错误码”、“错误简短信息”。

说明:关于 RPC 方法返回方式使用 Result 方式的理由:

1)使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。

2)如果不加栈信息,只是new自定义异常,加入自己的理解的error message,对于调用 端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输 的性能损耗也是问题。

Java开发规范(阿里巴巴)

78.stream优化利器:

特点:

  1. 不会改变源数据
  2. 不会存储数据,只负责计算
  3. 延迟操作,只有遇到了终止操作中间操作才会执行

优化利器toMap,groupingBy

避免for循环,使用map映射拿到数据
// 1.分组(一个key,多个value),value是对象其中一个属性
Map<String, List<Integer>> collect1 = stuList.stream()
        .collect(Collectors.groupingBy(Stu::getName, Collectors.mapping(Stu::getAge, Collectors.toList())));

// 2.分组,value是stu对象
Map<String, List<Stu>> collect2 = stuList.stream().collect(Collectors.groupingBy(Stu::getName));

// 3.集合转map处理(一个key,一个value)
注:key重复问题,value不能为null问题
Map<String, Integer> collect3 = stuList.stream()
        .collect(Collectors.toMap(Stu::getName, Stu::getAge, (key1, key2) -> key1));

// 4.集合求和,最大值,最小值,平均值
long sum = Arrays.asList(stu, stu2, stu3).stream().mapToLong(Stu::getAge).sum();

// 5.空值处理
String name =null;
String newName = Optional.of(name).orElse("空值");

// 6.使用stream流构造集合
// 7.mapToLong进行求和,最小值,最大值,平均值
Stream.of(1,2,3,4,5).mapToLong(Integer::longValue).min()/max()/count()/average();
Stream.of(1,2,3,4,5).mapToInt(Integer::intValue).sum();

// 8.peek操作用来打印、观察数据
Map<String, Integer> collect3 = stuList.stream()
    .peek(t -> {
        System.out.println("t = " + t);
    })
.collect(Collectors.toMap(Stu::getName, s -> s.getAge(), (key1, key2) -> key1));
posted @ 2025-02-22 23:02  永无八哥  阅读(38)  评论(0)    收藏  举报