使用java8实现List中对象属性的去重

今天在工作的时候遇到了一个问题,就是List的去重,不想用双重for,感觉太low,不想用for+Map,感觉应该有更好的方法,于是,google之。发现java8的stream流能完美解决这个问题。

1
List<BookInfoVo> list

比如在 BookInfoVo 中有一个 recordId 属性,现在需要对此去重.

怎么办呢?

有两种方法:

  • 第一种: 不使用java8 的 Stream
    1
    2
    3
    4
    5
    private List<BookInfoVo> removeDupliByRecordId(List<BookInfoVo> books) {
    Set<BookInfoVo> bookSet = new TreeSet<>((o1, o2)->o1.getRecordId().compareTo(o2.getRecordId()));
    personSet.addAll(books);
    return new ArrayList<BookInfoVo>(bookSet);
    }
    这也是大多数人第一想到的,借助 TreeSet 去重,其中 TreeSet 的其中一个构造函数接收一个排序的算法,同时这也会用到 TreeSet 的去重策略上.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /**
    * Constructs a new, empty tree set, sorted according to the specified
    * comparator. All elements inserted into the set must be <i>mutually
    * comparable</i> by the specified comparator: {@code comparator.compare(e1,
    * e2)} must not throw a {@code ClassCastException} for any elements
    * {@code e1} and {@code e2} in the set. If the user attempts to add
    * an element to the set that violates this constraint, the
    * {@code add} call will throw a {@code ClassCastException}.
    *
    * @param comparator the comparator that will be used to order this set.
    * If {@code null}, the {@linkplain Comparable natural
    * ordering} of the elements will be used.
    */
    public TreeSet(Comparator<? super E> comparator) {
    this(new TreeMap<>(comparator));
    }
  • 第二种: 炫酷的java8写法]
    1
    2
    3
    4
    5
    6
    /*方法二:炫酷的java8写法*/
    ArrayList<BookInfoVo> distinctLiost = list.stream()
    .collect(
    Collectors.collectingAndThen(
    Collectors.toCollection(() -> new TreeSet<>(Comparator.comparingLong(BookInfoVo::getRecordId))), ArrayList::new)
    );
    当然也可以根据多个属性去重
    1
    2
    ArrayList<BookInfoVo> distinctLiost = list.stream().collect(Collectors.collectingAndThen(
    Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(o -> o.getName() + ";" + o.getAuthor()))), ArrayList::new)
    如果没有第一种方法做铺垫,我们很可能一脸懵逼.

其实理解起来也不难:

关键在于Collectors.collectingAndThen( Collectors.toCollection(() -> new TreeSet<>(Comparator.comparingLong(BookInfoVo::getRecordId))), ArrayList::new)的理解,

collectingAndThen 这个方法的意思是: 将收集的结果转换为另一种类型: collectingAndThen,

因此上面的方法可以理解为,把 new TreeSet<>(Comparator.comparingLong(BookInfoVo::getRecordId))这个set转换为 ArrayList,这个结合第一种方法不难理解.

可以看到java8这种写法真是炫酷又强大!!!

l

java8类型互转

List< Integer >、int[ ]、Integer[ ]相互转换

[toc]

下文中出现的list、ints、integers分别代表一个列表、一个int数组、一个Integer数组。
它们之间所谓的转化,其实是 复制数据,互不干扰,可以理解为深拷贝。

int[ ] 转 List< Integer >

1
List<Integer> list = Arrays.stream(ints).boxed().collect(Collectors.toList());

Arrays.stream(ints) 之后返回的类型是 IntStream,IntStream 是一个接口继承自 BaseStream,BaseStream 又继承自AutoCloseable。所以我把IntStream看成一个方便对每个整数做操作的数据流。

之后调用了 boxed(),它的作用是对每个整数进行装箱,基本类型流转换为对象流,返回的是 Stream,Stream 也是继承自 BaseStream。所以这一步的作用是把 IntStream 转换成了 Stream

最后通过collect方法将数据流 (Stream) 收集成了集合 ( List),这里 collect 方法里传入的是一个收集器 (Collector),它通过 Collectors.toList() 产生。

小结:

  1. Arrays.stream(ints) 将基本类型数组转换为基本类型流。 int[ ] => IntStream
  2. .boxed() 将基本类型流转换为对象流。 => Stream< Integer >
  3. .collect(Collectors.toList()) 将对象流收集为集合。 => List< Integer >

int[ ] [ ] 转 List< List < Integer > >

1
2
String temp = Arrays.deepToString(fooArr).replaceAll("\\[","").replaceAll("\\]","");
List<String> fooList = new ArrayList<>(Arrays.asList(",")); //不对
1
List<List<Integer>> collect2 = Arrays.Stream(ints1).map(ar -> Arrays.stream(ar).boxed().collect(Collectors.toList())).collect(Collectors.toList());

int[ ] 转 Integer[ ]

1
Integer[] integers = Arrays.stream(ints).boxed().toArray(Integer[]::new);

一样的内容就不重复了,toArray(T[ ] :: new) 方法返回基本类型数组。
小结:

  1. Arrays.stream(ints) 将基本类型数组转换为基本类型流。 int[ ] => IntStream
  2. .boxed() 将基本类型流转换为对象流。=> Stream< Integer >
  3. .toArray(Integer[ ]::new) 将对象流转换为对象数组。=> Integer[ ]

int[ ] [ ]转 Integer[ ] [ ]

1
Integer[][] integers3 = Arrays.stream(ints1).map(ints -> Arrays.stream(ints).boxed().toArray(Integer[]::new)).toArray(Integer[][]::new);

Integer[ ] 转 List< Integer >

1
List<Integer> list = Arrays.asList(integers);

这个就很简单了,通过Arrays类里的asList方法将数组装换为List。值得注意:

asList 返回的是 Arrays 里的静态私有类 ArrayList,而不是 java.util 里的 ArrayList,它无法自动扩容。

可以用下面2种方法生成可扩容的ArrayList:

1
List<Integer> list = new ArrayList<>(Arrays.asList(integers)); 

或者

1
2
List<Integer> list = new ArrayList<>(); 
Collections.addAll(list, integers);

Integer[ ] [ ]转 List< List < Integer > >

1
2
Integer[][] a = {{1,2,3},{1,2,3}};
List<List<Integer>> lists = Arrays.stream(a).map(Arrays::asList).collect(Collectors.toList());

Integer[ ] 转 int[ ]

1
int[] ints = Arrays.stream(integers).mapToInt(Integer::valueOf).toArray();

map的意思是把每一个元素进行同样的操作。mapToInt的意思是把每一个元素转换为int。mapToInt(Integer::valueOf)方法返回的是IntStream。
小结:

  1. Arrays.stream(integers) 将对象数组转换为对象流。 Integer[ ] => Stream< Integer >
  2. .mapToInt(Integer::valueOf) 将对象流转换成基本类型流。=> IntStream
  3. .toArray() 将基本类型流转换为基本类型数组。 => int[ ]

Integer[ ] [ ]转 int[ ] [ ]

1
2
Integer[][] a = {{1,2,3},{1,2,3}};      
int[][] ints1 = Arrays.stream(a).map(a1 -> Arrays.stream(a1).mapToInt(Integer::valueOf).toArray()).toArray(int[][]::new);

List< Integer > 转 int[ ]

1
int[] ints = list.stream().mapToInt(Integer::valueOf).toArray();

经过上面的说明,相信这里已经很好理解了,直接小结。
小结:

  1. list.stream() 将列表转换为对象流。List< Integer > => Stream< Integer >
  2. .mapToInt(Integer::valueOf) 将对象流转换为基本数据类型流。=> IntStream
  3. .toArray() 将基本数据类型流转换为基本类型数组。=>int[ ]

List< List < Integer > > 转 int[ ] [ ]

1
2
3
4
List<List<Integer>> lists = ...;
int[][] arrays = lists.stream() // Stream<List<Integer>>
.map(list -> list.stream().mapToInt(i -> i).toArray()) // Stream<int[]>
.toArray(int[][]::new);

List< Integer > 转 Integer[ ]

1
Integer[] integers = list.toArray(new Integer[list.size()]);
1
Integer[] integers = list.stream().toArray(Integer[]::new); //不推荐

这个也很简单,方法里的参数是一个数组,所以要规定长度。也有无参的方法,但是要进行转型,所以不推荐使用。

List< List < Integer > > 转 Integer[ ] [ ]

1
2
3
4
List<List<Integer>> lists = ...;
Integer[][] arrays = lists.stream() // Stream<List<Integer>>
.map(list -> list.toArray(new Integer[list.size()])) // Stream<Integer[]>
.toArray(Integer[][]::new);
1
2
3
Integer[][] arrays = lists.stream()                                // Stream<List<Integer>>
.map(l -> l.stream().toArray(String[]::new)) // Stream<Integer[]>
.toArray(Integer[][]::new);
1
Integer[][] arrays = lists.stream().map(List::toArray).toArray(Integer[][]::new);

List< Character >、char[ ]、Character[ ]相互转换

char[ ] 转 List< Character >

1
2
char[] chars = {'a', 'b', 'c'};
List<Character> collect = new String(chars).chars().mapToObj(i -> (char) i).collect(Collectors.toList());

char[ ] [ ] 转 List< List < Character > >

1
List<List<Character>> collect2 = Arrays.stream(ints1).map(chars1 -> new String(chars1).chars().mapToObj(i -> (char) i).collect(Collectors.toList())).collect(Collectors.toList());

char[ ] 转 Character[ ]

1
2
char[] chars = {'a', 'b', 'c'};
Character[] characters1 = new String(chars).chars().mapToObj(i -> (char) i).toArray(Character[]::new);

char[ ] [ ]转 Character[ ] [ ]

1
Character[][] integers3 = Arrays.stream(ints1).map(chars -> new String(chars).chars().mapToObj(i->(char)i).toArray(Character[]::new)).toArray(Character[][]::new);

Character[ ] 转 List< Character >

1
List<Character> collect2 = Arrays.stream(characters).collect(Collectors.toList());

Character[ ] [ ]转 List< List < Character > >

1
List<List<Character>> lists = Arrays.stream(a).map(Arrays::asList).collect(Collectors.toList());

Character[ ] 转 char[ ]

1
2
char[] chars4 = Arrays.stream(characters1).map(String::valueOf).collect(Collectors.joining()).toCharArray();

Character[ ] [ ]转 char[ ] [ ]

1
char[][] ints1 = Arrays.stream(a).map(a1 -> Arrays.stream(a1).map(String::valueOf).collect(Collectors.joining()).toCharArray()).toArray(char[][]::new);

List< Character > 转 char[ ]

1
char[] value = characters.stream().map(String::valueOf).collect(Collectors.joining()).toCharArray();
1
2
Character[] charArr = characters.toArray(new Character[characters.size()]);
char[] value = ArrayUtils.toPrimitive(charArr);

List< List < Character > > 转 char[ ] [ ]

1
2
3
char[][] arrays = lists.stream()                                // Stream<List<Integer>>
.map(list -> list.stream().map(String::valueOf).collect(Collectors.joining()).toCharArray()) // Stream<int[]>
.toArray(char[][]::new);

List< Character > 转 Character[ ]

1
Character[] characters2 = Arrays.stream(characters1).toArray(Character[]::new);

List< List < Character > > 转 Character[ ] [ ]

1
2
Character[][] integers1 = lists.stream()                                // Stream<List<Integer>>
.map(list -> list.toArray(new Character[list.size()])).toArray(Character[][]::new);
1
2
3
Character[][] integers2 = lists.stream()                                // Stream<List<Integer>>
.map(l -> l.stream().toArray(Character[]::new))
.toArray(Character[][]::new);
1
Character[][] integers = lists.stream().map(List::toArray).toArray(Character[][]::new);
l

双亲委派机制

一、双亲委派机制

  1. class文件是通过类加载器加载到jvm中的

  2. 为了防止内存中存在多份同样的字节码,使用了双亲委派机制(不会自己加载类,而是把请求委托给父加载器去完成,依次向上)

  3. jdk本地方法类一般有根加载器(BootStrap Loader) 装载,jdk内部实现的扩展类一般由扩展加载器(ExtClassLoader),程序中的类文件则有系统加载器(AppClassLoader)装载

二、如何打破双亲委派机制

  1. 只要我加载类的时候,不是从AppClassLoader→ExtClassLoader→BootStrap Loader这个顺序找,那就是打破了。
  2. 因为加载class核心的方法在LoaderClass类的loadClass()方法上(双亲委派机制的核心实现上)
  3. 只要我自定义个ClassLoader,重写loadClass方法(不依照往上开始寻找类加载器),那就算是打破双亲委派机制了。

三、破坏双亲委派机制的场景

  1. tomcat:初学部署项目时,我们是把war包放到tomcat的webapp下,意味着tomcat可以运行多个web应用程序
  2. 那假设我现在有2个web应用程序,都有一个类,叫做User,并且它们的类全限定名都一样,比如:都是com.xxxxx.User。但是它们的具体实现是不一样的
  3. 那么tomcat是如何保证它们不会冲突的呢?
  4. 答案就是:tomcat给每个web应用创建了一个类加载器实例(WebAppClassLoader),该加载器重写了loadClass方法,优先加载当前应用目录下的类,如果当前找不到,才一层一层往上找。这样就做到了web应用层级的隔离。

四、tomcat还有别的类加载器吗

  1. 并不是web应用下的所有依赖都是需要隔离的,比如redis就是可以web应用之间共享的

  2. 因为如果版本相同,没必要每个web应用都独自加载一份

  3. 做法很简单,tomcat就在WebAppClassLoader上加了个父类加载器(SharedClassLoader),如果WebAppClassLoader自身没有加载到某个类,那就委托ShaerClassLoader去加载。(无非就是把需要应用程序之间需要共享的类放到一个共享目录下,SharedClassLoader)读共享目录的类就好了

  4. 为了隔离web应用与tomcat本身的类,又有类加载器(CatalinaClassLoader)来装载tomcat本身的依赖

  5. 如果tomcat本身的类的依赖和web应用还需要共享,那么还有类加载器(CommonClassLoader)来装载进而达到共享

  6. 各个类加载器的加载目录可以到tomcat的catalina.properties配置文件上查看

五、jdbc

  1. 有没有破坏双亲委派机制,见仁见智
  2. jdbc定义类接口,具体实现类由各个厂商进行实现(比如Mysql)
  3. 类加载有个规则:如果一个类由类加载器A加载,那么这个类的依赖类也是有相同的类加载器加载
  4. 我们用jdbc的时候,是使用DriverManager进而获取Connection,DriverManager在java.sql包下,显然是有BootStrap类加载器进行装载
  5. 当我们使用DriverManager.getConnection()时,得到的是一定是厂商实现的类
  6. 但BootStrap 加载器显然不可以加载各个厂商实现的类,这些实现类又没在java包中,怎么可能加载到呢
  7. DriverManager的解决方案是:在DriverManager初始化时,得到上下文加载器,去获取Connection时,是使用上下文加载器去加载Connection的,而这里的线程上下文加载器实际上还是(AppClassLoader)
  8. 在获取Connection的时候,还是先找到ExtClassLoader和B o o t S t ra p C la s sLoader,只不过这两加载器肯定是加载不到的,最终会有AppClassLoader进行加载
  9. 那这种情况,有的人觉得破坏了双亲委派机制,因为本来明明应该是有BootStrapClassLoader进行加载的,结果你来了一手线程上下文加载器,改掉了类加载器
  10. 有的人觉得没破坏双亲委派机制,只是改成了由线程上下文加载器进行类加载,但是还是遵守依次往上找父类加载,都找不到时才由自身加载。认为原则上没有改变。
  11. 我觉得这不重要,重要的是弄懂底层原理
l

线程与进程

一、进程

  1. 计算机内存空间

​ 用户空间装着用户进程需要使用的资源,比如你在程序代码里开一个数组, 这个数组肯定存在用户空间;内核空间存放内核进程需要加载的系统资源, 这一些资源一般是不允许用户访问的。但是注意有的用户进程会共享一些内 核空间的资源,比如一些动态链接库等等。

  1. 对于操作系统,进程就是一个数据结构,直接看 Linux 的源码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    struct task_struct {
    // 进程状态
    long state;
    // 虚拟内存结构体
    struct mm_struct *mm;
    // 进程号
    pid_t pid;
    // 指向父进程的指针
    struct task_struct __rcu *parent;
    // 一个数组,包含该进程打开的文件指针
    struct files_struct *files;
    };

    ​ 其中比较有意思的是 mm 指针和 files 指针。

    ​ mm 指针指向:进程的虚拟内存,也就是载入资源和可执行文件的地方;

    ​ files 指针指向:一个数组,这个数组里装着所有该进程打开的文件的指针。

二、文件描述符

每个进程被创建时, files 的前三位被填入默认值,分别指向标准输入 流、标准输出流、标准错误流。我们常说的「文件描述符」就是指这个文件 指针数组的索引(0,1,2),所以程序的文件描述符默认情况下 0 是输入,1 是输出, 2 是错误。

​ linux一切皆文件,对于一般的计算机,输入流是键盘,输出流是显示器,错误流也是显示器, 所以现在这个进程和内核连了三根线。因为硬件都是由内核管理的,我们的 进程需要通过「系统调用」让内核进程访问硬件资源。

​ 如果我们写的程序需要其他资源,比如打开一个文件进行读写,这也很简 单,进行系统调用,让内核把文件打开,这个文件就会被放到 files 的第 4 个位置:

输入重定向:command < file.txt,file[0]指向file.txt,程序从file[0]读取数据

输出重定向:command > file.txt,file[1]指向file.txt, 程序像file[1]写入数据

管道符:cmd1 | cmd2 把一个进程的输出流和另一个进程的输入流接起 一条「管道」,数据就在其中传递

注意:一个简单的 files 数组,进程通过简单的文件描述符访问相应资源, 具体细节交于操作系统,有效解耦,优美高效。

三、线程是什么

​ 之所以Linux 中线程和进程基本没有区别呢,因为从 Linux 内核的角度来看,并没有把线程和进程区别对待。都是用 task_struct 结构表示的,唯一的 区别就是共享的数据区域不同

​ 换句话说,线程看起来跟进程没有区别,只是线程的某些数据区域和其父进 程是共享的,而子进程是拷⻉副本,而不是共享。就比如说, mm 结构 和 files 结构在线程中都是共享的,我画两张图你就明白了:

注意:对于新建进程时内存区域拷 ⻉的问题,Linux 采用了 copy-on-write 的策略优化,也就是并不真正复制父 进程的内存空间,而是等到需要写操作时才去复制。所以 Linux 中新建进 程和新建线程都是很迅速的

l

注释书写规范

  • 一般情况下,源程序有效注释量必须在30%以上。注释的原则是有助于对程序的阅读理解,在该加的地方都加了,注释不宜太多也不能太少,注释语言必须准确、易懂、简洁。可以用注释统计工具来统计。

    一、类和接口的注释

1. 类外注释

该注释放在 package 关键字之后,class 或者 interface 关键字之前。

  • 说明:方便JavaDoc收集。
    示例:
1
2
3
4
5
package com.huawei.msg.relay.comm;
/**
* 注释内容
*/
public class CommManager
  • 类和接口的注释内容:类的注释主要是一句话功能简述、功能详细描述。
    格式:
    1
    2
    3
    4
    /**
    * 〈一句话功能简述〉
    * 〈功能详细描述〉
    */
  • 描述部分说明该类或者接口的功能、作用、使用方法和注意事项。
    示例:
    1
    2
    3
    4
    5
    /**
    * LogManager 类集中控制对日志读写的操作。
    * 全部为静态变量和静态方法,对外提供统一接口。分配对应日志类型的读写器,
    * 读取或写入符合条件的日志纪录。
    */

2. 类内注释

类属性、公有和保护方法必须写注释。geter、seter方法不用写注释

示例:

1
2
3
4
5
6
7
8
/**
* 注释内容
*/
private String logType;
/**
* 注释内容
*/
public void write()

1).成员变量注释内容:成员变量的意义、目的、功能,可能被用到的地方。
2).公有和保护方法注释内容:列出方法的一句话功能 简述、功能详细描述、输入参数、输出参数、返回值、违例等。
格式:

1
2
3
4
5
6
7
8
/**
* 〈一句话功能简述〉
* 〈功能详细描述〉
* @param [参数1] [in或out] [参数1说明]
* @param [参数2] [in或out] [参数2说明]
* @return [返回类型说明]
* @exception/throws [违例类型] [违例说明]
*/

说明: @exception或throws 列出可能仍出的异常。
示例:

1
2
3
4
5
6
7
/**
* 用MD5算法计算输入字符串的32位摘要
* @param sIn [in] 待处理的字符串
* @param sOut [out] sIn的32为摘要,调用函数负责new sOut对象
* @return boolean
*/
public static boolean getMd5(String sIn, StringBuffer sOut)

说明:
1).注释应与其描述的代码相近,对代码的注释应放在其上方或右方(对单条语句的注释)相邻位置,不可放在下面,如放于上方则需与其上面的代码用空行隔开。
2).注释与所描述内容进行同样的缩排。
3).将注释与其上面的代码用空行隔开。
示例:

1
2
3
4
5
//注释
program code one
(空一格)
//注释
program code two

二、方法与复杂逻辑的注释

对变量的定义和分支语句(条件分支、循环语句等)对复杂的分支必须编写注释,如果时间允许,建议对所有分支语句写注释。

说明:这些语句往往是程序实现某一特定功能的关键,对于维护人员来说,良好的注释帮助更好的理解程序,有时甚至优于看设计文档。

  1. switch语句
  • switch语句下的case语句,如果因为特殊情况需要处理完一个case后进入下一个case处理,必须在该case语句处理完、下一个case语句前加上明确的注释。
  • 说明:这样比较清楚程序编写者的意图,有效防止无故遗漏break语句。
  1. 边写代码边注释
  • 修改代码同时修改相应的注释,以保证注释与代码的一致性。不再有用的注释要删除。
  1. 避免在注释中使用缩写
  • 在使用缩写时或之前,应对缩写进行必要的说明,特别是不常用缩写。
  1. 用中文注释,禁止用英文写注释。

    建议

  2. 避免在一行代码或表达式的中间插入注释。
  • 说明:除非必要,不应在代码或表达中间插入注释,否则容易使代码可理解性变差。
  1. 通过对函数或过程、变量、结构等正确的命名以及合理地组织代码的结构,使代码成为自注释的。
  • 说明:清晰准确的函数、变量等的命名,可增加代码可读性,并减少不必要的注释。
  1. 在代码的功能、意图层次上进行注释,提供有用、额外的信息。
  • 说明:注释的目的是解释代码的目的、功能和采用的方法,提供代码以外的信息,帮助读者理解代码,防止没必要的重复注释信息。
  • 示例:如下注释意义不大。
    1
    2
    // 如果 receiveFlag 为真
    if (receiveFlag)
  • 而如下的注释则给出了额外有用的信息。
    1
    2
    // 如果从连结收到消息 
    if (receiveFlag)
  1. 在程序块的结束行右方加注释标记,以表明某程序块的结束。
  • 说明:当代码段较长,特别是多重嵌套时,这样做可以使代码更清晰,更便于阅读。
  • 示例:参见如下例子。
    1
    2
    3
    4
    5
    6
    7
    8
    if (...)
    {
    program code1
    while (index < MAX_INDEX)
    {
    program code2
    } // end of while (index < MAX_INDEX) // 指明该条while语句结束
    } // end of if (...) // 指明是哪条if语句结束
  1. 方法内的单行注释使用 //。
  • 说明:调试程序的时候可以方便的使用 /* 。。。*/ 注释掉一长段程序。
  1. 注释使用中文注释和中文标点,不得用英文写注释。方法和类描述的第一句话尽量使用简洁明了的话概括一下功能,然后加以句号。接下来的部分可以详细描述。
  • 说明:JavaDoc工具收集简介的时候使用选取第一句话。
  1. 顺序实现流程的说明使用1、2、3、4在每个实现步骤部分的代码前面进行注释。
  • 示例:如下是对设置属性的流程注释
    1
    2
    3
    4
    //1、 判断输入参数是否有效。
    ...
    // 2、设置本地变量。
    ...
  1. 一些复杂的算法代码需要说明。
  • 示例:这里主要是对闰年算法的说明。
    java //1. 如果能被4整除,是闰年; //2. 如果能被100整除,不是闰年.; //3. 如果能被400整除,是闰年.。 星期五, 23. 八月 2019 07:42下午

**


**

l

面试官:说说你了解class文件吗?

本文思维导图:

https://cdn.jsdelivr.net/gh/swimminghao/picture@main/img/2rLXEs_20210507091957.png

Class类文件结构

为什么Java可以一次编译到处运行?JVM无关性

与平台无关性是建立在操作系统上,虚拟机厂商提供了许多可以运行在各种不同平台的虚拟机,它们都可以载入和执行字节码,从而实现程序的“一次编写,到处运行”。

各种不同平台的虚拟机与所有平台都统一使用的程序存储格式——字节码(Byte Code)是构成平台无关性的基石,也是语言无关性的基础。Java 虚拟机不和包括 Java 在内的任何语言绑定,它只与“Class 文件”这种特定的二进制文件格式所关联,Class 文件中包含了 Java 虚拟机指令集和符号表以及若干其他辅助信息。

https://cdn.jsdelivr.net/gh/swimminghao/picture@main/img/ZGOAdI_20210507092208.png

Class 类文件

Java 技术能够一直保持非常好的向后兼容性,这点 Class 文件结构的稳定性功不可没。Java 已经发展到 14 版本,但是 class 文件结构的内容,绝大部分在JDK1.2 时代就已经定义好了。虽然 JDK1.2 的内容比较古老,但是 java 发展经历了十余个大版本,但是每次基本上知识在原有结构基础上新增内容、扩充功能,并未对定义的内容做修改。

任何一个 Class 文件都对应着唯一一个类或接口的定义信息,但反过来说,Class 文件实际上它并不一定以磁盘文件的形式存在(比如可以动态生成、或者直接送入类加载器中)。

Class 文件是一组以 8 位字节为基础单位的二进制流。

工具介绍

Sublime:查看 16 进制的编辑器
javap:javap 是 JDK 自带的反解析工具。它的作用是将 .class 字节码文件解析成可读的文件格式。
在使用 javap 时我一般会添加 -v 参数,尽量多打印一些信息。同时,我也会使用 -p 参数,打印一些私有的字段和方法。
jclasslib:如果你不太习惯使用命令行的操作,还可以使用 jclasslib,jclasslib 是一个图形化的工具,能够更加直观的查看字节码中的内容。它还分门别类的对类中的各个部分进行了整理,非常的人性化。同时,它还提供了 Idea 的插件,你可以从 plugins 中搜索到它。

Class 文件格式

从一个 Class 文件开始,整个 Class 文件的格式就是一个二进制的字节流。各个数据项目严格按照顺序紧凑地排列在 Class 文件之中,中间没有添加任何分隔符,这使得整个 Class 文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。

Class 文件格式采用一种类似于 C 语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。

无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表 1 个字节(一个字节是由两位 16 进制数组成 (1个16进制数=4个二进制数 8个二进制数=一个字节))、2 个字节、4 个字节和 8 个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串值。

表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class 文件本质上就是一张表。

Class 文件格式详解

Class 的结构不像 XML 等描述语言,由于它没有任何分隔符号,所以在其中的数据项,无论是顺序还是数量,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。

https://cdn.jsdelivr.net/gh/swimminghao/picture@main/img/mwHImZ_20210507115741.png

按顺序包括:

魔数与 Class 文件的版本

每个 Class 文件的头 4 个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件。使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。文件格式的制定者可以自由地选择魔数值,只要这个魔数值还没有被广泛采用过同时又不会引起混淆即可。

紧接着魔数的 4 个字节存储的是 Class 文件 的版本号:第 5 和第 6 个字节是次版本号(MinorVersion),第 7 和第 8 个字节是主版本号(Major Version)。

Java 的版本号是从 45 开始的,JDK 1.1 之后的每个 JDK 大版本发布主版本号向上加 1 高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能运行以后版本的 Class 文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的 Class 文件。代表 JDK1.8(16 进制的 34,换成 10 进制就是 52)

常量池

常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项 u2 类型的数据,代表常量池容量计数值(constant_pool_count)。与 Java 中语言习惯不一样的是,这个容量计数是从 1 而不是 0 开始的

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。
字面量比较接近于 Java 语言层面的常量概念,如文本字符串、声明为 final 的常量值等。
符号引用则属于编译原理方面的概念,包括了下面三类常量:类和接口的全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符

访问标志

用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口;是否定义为 public 类型;是否定义为 abstract 类型;如果是类的话,是否被声明为 final 等

类索引、父类索引与接口索引集合

这三项数据来确定类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于 Java 语言不允许多重继承,所以父类索引只有一个,除了 java.lang.Object 之外,所有的 Java 类都有父类,因此除了java.lang.Object 外,所有 Java 类的父类索引都不为 0。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按 implements 语句(如果这个类本身是一个接口,则应当是 extends 语句)后的接口顺序从左到右排列在接口索引集合中

字段表集合

描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量。
而字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本 Java 代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。

方法表集合

描述了方法的定义,但是方法里的 Java 代码,经过编译器编译成字节码指令后,存放在属性表集合中的方法属性表集合中一个名为“Code”的属性里面。

与字段表集合相类似的,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器“<clinit>”方法和实例构造器“<init>”

属性表集合

存储 Class 文件、字段表、方法表都自己的属性表集合,以用于描述某些场景专有的信息。如方法的代码就存储在 Code 属性表中。

l

使用java8实现List中对象属性的去重

今天在工作的时候遇到了一个问题,就是List的去重,不想用双重for,感觉太low,不想用for+Map,感觉应该有更好的方法,于是,google之。发现java8的stream流能完美解决这个问题。

1
List<BookInfoVo> list

比如在 BookInfoVo 中有一个 recordId 属性,现在需要对此去重.

怎么办呢?

有两种方法:

  • 第一种: 不使用java8 的 Stream
    1
    2
    3
    4
    5
    private List<BookInfoVo> removeDupliByRecordId(List<BookInfoVo> books) {
    Set<BookInfoVo> bookSet = new TreeSet<>((o1, o2)->o1.getRecordId().compareTo(o2.getRecordId()));
    personSet.addAll(books);
    return new ArrayList<BookInfoVo>(bookSet);
    }
    这也是大多数人第一想到的,借助 TreeSet 去重,其中 TreeSet 的其中一个构造函数接收一个排序的算法,同时这也会用到 TreeSet 的去重策略上.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /**
    * Constructs a new, empty tree set, sorted according to the specified
    * comparator. All elements inserted into the set must be <i>mutually
    * comparable</i> by the specified comparator: {@code comparator.compare(e1,
    * e2)} must not throw a {@code ClassCastException} for any elements
    * {@code e1} and {@code e2} in the set. If the user attempts to add
    * an element to the set that violates this constraint, the
    * {@code add} call will throw a {@code ClassCastException}.
    *
    * @param comparator the comparator that will be used to order this set.
    * If {@code null}, the {@linkplain Comparable natural
    * ordering} of the elements will be used.
    */
    public TreeSet(Comparator<? super E> comparator) {
    this(new TreeMap<>(comparator));
    }
  • 第二种: 炫酷的java8写法]
    1
    2
    3
    4
    5
    6
    /*方法二:炫酷的java8写法*/
    ArrayList<BookInfoVo> distinctLiost = list.stream()
    .collect(
    Collectors.collectingAndThen(
    Collectors.toCollection(() -> new TreeSet<>(Comparator.comparingLong(BookInfoVo::getRecordId))), ArrayList::new)
    );
    当然也可以根据多个属性去重
    1
    2
    ArrayList<BookInfoVo> distinctLiost = list.stream().collect(Collectors.collectingAndThen(
    Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(o -> o.getName() + ";" + o.getAuthor()))), ArrayList::new)
    如果没有第一种方法做铺垫,我们很可能一脸懵逼.

其实理解起来也不难:

关键在于Collectors.collectingAndThen( Collectors.toCollection(() -> new TreeSet<>(Comparator.comparingLong(BookInfoVo::getRecordId))), ArrayList::new)的理解,

collectingAndThen 这个方法的意思是: 将收集的结果转换为另一种类型: collectingAndThen,

因此上面的方法可以理解为,把 new TreeSet<>(Comparator.comparingLong(BookInfoVo::getRecordId))这个set转换为 ArrayList,这个结合第一种方法不难理解.

可以看到java8这种写法真是炫酷又强大!!!

l

java8类型互转

List< Integer >、int[ ]、Integer[ ]相互转换

[toc]

下文中出现的list、ints、integers分别代表一个列表、一个int数组、一个Integer数组。
它们之间所谓的转化,其实是 复制数据,互不干扰,可以理解为深拷贝。

int[ ] 转 List< Integer >

1
List<Integer> list = Arrays.stream(ints).boxed().collect(Collectors.toList());

Arrays.stream(ints) 之后返回的类型是 IntStream,IntStream 是一个接口继承自 BaseStream,BaseStream 又继承自AutoCloseable。所以我把IntStream看成一个方便对每个整数做操作的数据流。

之后调用了 boxed(),它的作用是对每个整数进行装箱,基本类型流转换为对象流,返回的是 Stream,Stream 也是继承自 BaseStream。所以这一步的作用是把 IntStream 转换成了 Stream

最后通过collect方法将数据流 (Stream) 收集成了集合 ( List),这里 collect 方法里传入的是一个收集器 (Collector),它通过 Collectors.toList() 产生。

小结:

  1. Arrays.stream(ints) 将基本类型数组转换为基本类型流。 int[ ] => IntStream
  2. .boxed() 将基本类型流转换为对象流。 => Stream< Integer >
  3. .collect(Collectors.toList()) 将对象流收集为集合。 => List< Integer >

int[ ] [ ] 转 List< List < Integer > >

1
2
String temp = Arrays.deepToString(fooArr).replaceAll("\\[","").replaceAll("\\]","");
List<String> fooList = new ArrayList<>(Arrays.asList(",")); //不对
1
List<List<Integer>> collect2 = Arrays.Stream(ints1).map(ar -> Arrays.stream(ar).boxed().collect(Collectors.toList())).collect(Collectors.toList());

int[ ] 转 Integer[ ]

1
Integer[] integers = Arrays.stream(ints).boxed().toArray(Integer[]::new);

一样的内容就不重复了,toArray(T[ ] :: new) 方法返回基本类型数组。
小结:

  1. Arrays.stream(ints) 将基本类型数组转换为基本类型流。 int[ ] => IntStream
  2. .boxed() 将基本类型流转换为对象流。=> Stream< Integer >
  3. .toArray(Integer[ ]::new) 将对象流转换为对象数组。=> Integer[ ]

int[ ] [ ]转 Integer[ ] [ ]

1
Integer[][] integers3 = Arrays.stream(ints1).map(ints -> Arrays.stream(ints).boxed().toArray(Integer[]::new)).toArray(Integer[][]::new);

Integer[ ] 转 List< Integer >

1
List<Integer> list = Arrays.asList(integers);

这个就很简单了,通过Arrays类里的asList方法将数组装换为List。值得注意:

asList 返回的是 Arrays 里的静态私有类 ArrayList,而不是 java.util 里的 ArrayList,它无法自动扩容。

可以用下面2种方法生成可扩容的ArrayList:

1
List<Integer> list = new ArrayList<>(Arrays.asList(integers)); 

或者

1
2
List<Integer> list = new ArrayList<>(); 
Collections.addAll(list, integers);

Integer[ ] [ ]转 List< List < Integer > >

1
2
Integer[][] a = {{1,2,3},{1,2,3}};
List<List<Integer>> lists = Arrays.stream(a).map(Arrays::asList).collect(Collectors.toList());

Integer[ ] 转 int[ ]

1
int[] ints = Arrays.stream(integers).mapToInt(Integer::valueOf).toArray();

map的意思是把每一个元素进行同样的操作。mapToInt的意思是把每一个元素转换为int。mapToInt(Integer::valueOf)方法返回的是IntStream。
小结:

  1. Arrays.stream(integers) 将对象数组转换为对象流。 Integer[ ] => Stream< Integer >
  2. .mapToInt(Integer::valueOf) 将对象流转换成基本类型流。=> IntStream
  3. .toArray() 将基本类型流转换为基本类型数组。 => int[ ]

Integer[ ] [ ]转 int[ ] [ ]

1
2
Integer[][] a = {{1,2,3},{1,2,3}};      
int[][] ints1 = Arrays.stream(a).map(a1 -> Arrays.stream(a1).mapToInt(Integer::valueOf).toArray()).toArray(int[][]::new);

List< Integer > 转 int[ ]

1
int[] ints = list.stream().mapToInt(Integer::valueOf).toArray();

经过上面的说明,相信这里已经很好理解了,直接小结。
小结:

  1. list.stream() 将列表转换为对象流。List< Integer > => Stream< Integer >
  2. .mapToInt(Integer::valueOf) 将对象流转换为基本数据类型流。=> IntStream
  3. .toArray() 将基本数据类型流转换为基本类型数组。=>int[ ]

List< List < Integer > > 转 int[ ] [ ]

1
2
3
4
List<List<Integer>> lists = ...;
int[][] arrays = lists.stream() // Stream<List<Integer>>
.map(list -> list.stream().mapToInt(i -> i).toArray()) // Stream<int[]>
.toArray(int[][]::new);

List< Integer > 转 Integer[ ]

1
Integer[] integers = list.toArray(new Integer[list.size()]);
1
Integer[] integers = list.stream().toArray(Integer[]::new); //不推荐

这个也很简单,方法里的参数是一个数组,所以要规定长度。也有无参的方法,但是要进行转型,所以不推荐使用。

List< List < Integer > > 转 Integer[ ] [ ]

1
2
3
4
List<List<Integer>> lists = ...;
Integer[][] arrays = lists.stream() // Stream<List<Integer>>
.map(list -> list.toArray(new Integer[list.size()])) // Stream<Integer[]>
.toArray(Integer[][]::new);
1
2
3
Integer[][] arrays = lists.stream()                                // Stream<List<Integer>>
.map(l -> l.stream().toArray(String[]::new)) // Stream<Integer[]>
.toArray(Integer[][]::new);
1
Integer[][] arrays = lists.stream().map(List::toArray).toArray(Integer[][]::new);

List< Character >、char[ ]、Character[ ]相互转换

char[ ] 转 List< Character >

1
2
char[] chars = {'a', 'b', 'c'};
List<Character> collect = new String(chars).chars().mapToObj(i -> (char) i).collect(Collectors.toList());

char[ ] [ ] 转 List< List < Character > >

1
List<List<Character>> collect2 = Arrays.stream(ints1).map(chars1 -> new String(chars1).chars().mapToObj(i -> (char) i).collect(Collectors.toList())).collect(Collectors.toList());

char[ ] 转 Character[ ]

1
2
char[] chars = {'a', 'b', 'c'};
Character[] characters1 = new String(chars).chars().mapToObj(i -> (char) i).toArray(Character[]::new);

char[ ] [ ]转 Character[ ] [ ]

1
Character[][] integers3 = Arrays.stream(ints1).map(chars -> new String(chars).chars().mapToObj(i->(char)i).toArray(Character[]::new)).toArray(Character[][]::new);

Character[ ] 转 List< Character >

1
List<Character> collect2 = Arrays.stream(characters).collect(Collectors.toList());

Character[ ] [ ]转 List< List < Character > >

1
List<List<Character>> lists = Arrays.stream(a).map(Arrays::asList).collect(Collectors.toList());

Character[ ] 转 char[ ]

1
2
char[] chars4 = Arrays.stream(characters1).map(String::valueOf).collect(Collectors.joining()).toCharArray();

Character[ ] [ ]转 char[ ] [ ]

1
char[][] ints1 = Arrays.stream(a).map(a1 -> Arrays.stream(a1).map(String::valueOf).collect(Collectors.joining()).toCharArray()).toArray(char[][]::new);

List< Character > 转 char[ ]

1
char[] value = characters.stream().map(String::valueOf).collect(Collectors.joining()).toCharArray();
1
2
Character[] charArr = characters.toArray(new Character[characters.size()]);
char[] value = ArrayUtils.toPrimitive(charArr);

List< List < Character > > 转 char[ ] [ ]

1
2
3
char[][] arrays = lists.stream()                                // Stream<List<Integer>>
.map(list -> list.stream().map(String::valueOf).collect(Collectors.joining()).toCharArray()) // Stream<int[]>
.toArray(char[][]::new);

List< Character > 转 Character[ ]

1
Character[] characters2 = Arrays.stream(characters1).toArray(Character[]::new);

List< List < Character > > 转 Character[ ] [ ]

1
2
Character[][] integers1 = lists.stream()                                // Stream<List<Integer>>
.map(list -> list.toArray(new Character[list.size()])).toArray(Character[][]::new);
1
2
3
Character[][] integers2 = lists.stream()                                // Stream<List<Integer>>
.map(l -> l.stream().toArray(Character[]::new))
.toArray(Character[][]::new);
1
Character[][] integers = lists.stream().map(List::toArray).toArray(Character[][]::new);
l

注释书写规范

  • 一般情况下,源程序有效注释量必须在30%以上。注释的原则是有助于对程序的阅读理解,在该加的地方都加了,注释不宜太多也不能太少,注释语言必须准确、易懂、简洁。可以用注释统计工具来统计。

    一、类和接口的注释

1. 类外注释

该注释放在 package 关键字之后,class 或者 interface 关键字之前。

  • 说明:方便JavaDoc收集。
    示例:
1
2
3
4
5
package com.huawei.msg.relay.comm;
/**
* 注释内容
*/
public class CommManager
  • 类和接口的注释内容:类的注释主要是一句话功能简述、功能详细描述。
    格式:
    1
    2
    3
    4
    /**
    * 〈一句话功能简述〉
    * 〈功能详细描述〉
    */
  • 描述部分说明该类或者接口的功能、作用、使用方法和注意事项。
    示例:
    1
    2
    3
    4
    5
    /**
    * LogManager 类集中控制对日志读写的操作。
    * 全部为静态变量和静态方法,对外提供统一接口。分配对应日志类型的读写器,
    * 读取或写入符合条件的日志纪录。
    */

2. 类内注释

类属性、公有和保护方法必须写注释。geter、seter方法不用写注释

示例:

1
2
3
4
5
6
7
8
/**
* 注释内容
*/
private String logType;
/**
* 注释内容
*/
public void write()

1).成员变量注释内容:成员变量的意义、目的、功能,可能被用到的地方。
2).公有和保护方法注释内容:列出方法的一句话功能 简述、功能详细描述、输入参数、输出参数、返回值、违例等。
格式:

1
2
3
4
5
6
7
8
/**
* 〈一句话功能简述〉
* 〈功能详细描述〉
* @param [参数1] [in或out] [参数1说明]
* @param [参数2] [in或out] [参数2说明]
* @return [返回类型说明]
* @exception/throws [违例类型] [违例说明]
*/

说明: @exception或throws 列出可能仍出的异常。
示例:

1
2
3
4
5
6
7
/**
* 用MD5算法计算输入字符串的32位摘要
* @param sIn [in] 待处理的字符串
* @param sOut [out] sIn的32为摘要,调用函数负责new sOut对象
* @return boolean
*/
public static boolean getMd5(String sIn, StringBuffer sOut)

说明:
1).注释应与其描述的代码相近,对代码的注释应放在其上方或右方(对单条语句的注释)相邻位置,不可放在下面,如放于上方则需与其上面的代码用空行隔开。
2).注释与所描述内容进行同样的缩排。
3).将注释与其上面的代码用空行隔开。
示例:

1
2
3
4
5
//注释
program code one
(空一格)
//注释
program code two

二、方法与复杂逻辑的注释

对变量的定义和分支语句(条件分支、循环语句等)对复杂的分支必须编写注释,如果时间允许,建议对所有分支语句写注释。

说明:这些语句往往是程序实现某一特定功能的关键,对于维护人员来说,良好的注释帮助更好的理解程序,有时甚至优于看设计文档。

  1. switch语句
  • switch语句下的case语句,如果因为特殊情况需要处理完一个case后进入下一个case处理,必须在该case语句处理完、下一个case语句前加上明确的注释。
  • 说明:这样比较清楚程序编写者的意图,有效防止无故遗漏break语句。
  1. 边写代码边注释
  • 修改代码同时修改相应的注释,以保证注释与代码的一致性。不再有用的注释要删除。
  1. 避免在注释中使用缩写
  • 在使用缩写时或之前,应对缩写进行必要的说明,特别是不常用缩写。
  1. 用中文注释,禁止用英文写注释。

    建议

  2. 避免在一行代码或表达式的中间插入注释。
  • 说明:除非必要,不应在代码或表达中间插入注释,否则容易使代码可理解性变差。
  1. 通过对函数或过程、变量、结构等正确的命名以及合理地组织代码的结构,使代码成为自注释的。
  • 说明:清晰准确的函数、变量等的命名,可增加代码可读性,并减少不必要的注释。
  1. 在代码的功能、意图层次上进行注释,提供有用、额外的信息。
  • 说明:注释的目的是解释代码的目的、功能和采用的方法,提供代码以外的信息,帮助读者理解代码,防止没必要的重复注释信息。
  • 示例:如下注释意义不大。
    1
    2
    // 如果 receiveFlag 为真
    if (receiveFlag)
  • 而如下的注释则给出了额外有用的信息。
    1
    2
    // 如果从连结收到消息 
    if (receiveFlag)
  1. 在程序块的结束行右方加注释标记,以表明某程序块的结束。
  • 说明:当代码段较长,特别是多重嵌套时,这样做可以使代码更清晰,更便于阅读。
  • 示例:参见如下例子。
    1
    2
    3
    4
    5
    6
    7
    8
    if (...)
    {
    program code1
    while (index < MAX_INDEX)
    {
    program code2
    } // end of while (index < MAX_INDEX) // 指明该条while语句结束
    } // end of if (...) // 指明是哪条if语句结束
  1. 方法内的单行注释使用 //。
  • 说明:调试程序的时候可以方便的使用 /* 。。。*/ 注释掉一长段程序。
  1. 注释使用中文注释和中文标点,不得用英文写注释。方法和类描述的第一句话尽量使用简洁明了的话概括一下功能,然后加以句号。接下来的部分可以详细描述。
  • 说明:JavaDoc工具收集简介的时候使用选取第一句话。
  1. 顺序实现流程的说明使用1、2、3、4在每个实现步骤部分的代码前面进行注释。
  • 示例:如下是对设置属性的流程注释
    1
    2
    3
    4
    //1、 判断输入参数是否有效。
    ...
    // 2、设置本地变量。
    ...
  1. 一些复杂的算法代码需要说明。
  • 示例:这里主要是对闰年算法的说明。
    java //1. 如果能被4整除,是闰年; //2. 如果能被100整除,不是闰年.; //3. 如果能被400整除,是闰年.。 星期五, 23. 八月 2019 07:42下午

**


**

l

10、【对线面试官】TreadLocal

今天要不来聊聊ThreadLocal吧?

  1. 我个人对ThreadLocal理解就是
  2. 它能够提供了线程的局部变量让每个线程都可以通过set/get来对这个局部变量进行操作
  3. 不会和其他线程的局部变量进行冲突,实现了线程的数据隔离

你在工作中有用到过ThreadLocal吗?

  1. 这块是真不多,不过还是有一处的。就是我们项目有个的DateUtils工具类
  2. 这个工具类主要是对时间进行格式化
  3. 格式化/转化的实现是用的SimpleDateFormat
  4. 但众所周知SimpleDateFormat不是线程安全的 ,所以我们就用ThreadLocal来让每个线程装载着自己的SimpleDateFormat对象
  5. 以达到在格式化时间时,线程安全的目的
  6. 在方法上创建SimpleDateFormat对象也没问题,但每调用一次就创建一次有点不优雅
  7. 在工作中ThreadLocal的应用场景确实不多,但要不我给你讲讲Spring是怎么用的?

spring中的应用

  1. Spring提供了事务相关的操作,而我们知道事务是得保证一组操作同时成功或失败的
  2. 这意味着我们一次事务的所有操作需要在同一个数据库连接上
  3. 但是在我们日常写代码的时候是不需要关注这点的
  4. Spring就是用的ThreadLocal来实现,Th readLocal存储的类型是一个Map
  5. Map中的key是DataSource,value是C onnection(为了应对多数据源的情况,所以是一个Map)
  6. 用了ThreadLocal保证了同一个线程获取一个Connection对象,从而保证一次事务的所有操作需要在同一个数据库连接上

你知道ThreadLocal内存泄露这个知识点吗?

  1. 了解的,要不我先来讲讲ThreadLocal的原理?
    • ThreadLocal是一个壳子,真正的存储结构是ThreadLocal里有ThreadLocalMap这么个内部类
    • 而有趣的是,ThreadLocalMap的引用是在Thread上定义的
    • ThreadLocal本身并不存储值,它只是作为key来让线程从ThreadLocalMap获取value
    • 所以,得出的结论就是ThreadLocalMap该结构本身就在Thread下定义,而ThreadLocal只是作为key,存储set到ThreadLocalMap的变量当然是线程私有的咯

l