0%

前言

说真的,平常看源码都是自己看完自己懂,很少有写出来的冲动。
但是在写算法的时候,经常用到java中各种集合,其中也比较常用到remove方法。
remove有重载函数,分别传入参数是索引index或者数据Object(指定泛型后自动转换),如果指定泛型是其他数据类型还好,但是指定的是Integer或者是int的话,或者就有点懵了。
这曾经也困惑过我,所以我就唯有用实践解惑了。

测试类设计

  • 测试类一

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class Text {

    public void remove(int index){
    System.out.println("调用传参为int的remove方法");
    }

    public void remove(Integer object){
    System.out.println("调用传参为Integer的remove方法");
    }

    public void remove(Object object){
    System.out.println("调用传参为Object的remove方法");
    }
    }
  • 测试类二

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class Text {

    public void remove(Integer object){
    System.out.println("调用传参为Integer的remove方法");
    }

    public void remove(Object object){
    System.out.println("调用传参为Object的remove方法");
    }
    }
  • 测试类三

    1
    2
    3
    4
    5
    6
    public class Text {

    public void remove(Object object){
    System.out.println("调用传参为Object的remove方法");
    }
    }

结果

三个测试类分别传入int,Integer,Object型变量,观察效果。

  • 测试类一

    传入类型为int:调用传参为int的remove方法
    传入类型为Integer:调用传参为Integer的remove方法
    传入类型为Object:调用传参为Object的remove方法

  • 测试类二

    传入类型为int:调用传参为Integer的remove方法
    传入类型为Integer:调用传参为Integer的remove方法
    传入类型为Object:调用传参为Object的remove方法

  • 测试类三

    传入类型为int:调用传参为Object的remove方法
    传入类型为Integer:调用传参为Object的remove方法
    传入类型为Object:调用传参为Object的remove方法

从输出结果可以看出,当方法的传参的类层级逐渐变高时,层级较低的传参会进行向上转型适应传参的需要。

原因分析

下面我们先反编译各测试类的源码,结果如下

  • 测试类一

    invokevirtual #11 // Method remove:(I)V

    invokevirtual #15 // Method remove:(Ljava/lang/Integer;)V

    invokevirtual #18 // Method remove:(Ljava/lang/Object;)V

  • 测试类二

    invokevirtual #11 // Method remove:(Ljava/lang/Integer;)V

    invokevirtual #11 // Method remove:(Ljava/lang/Integer;)V

    invokevirtual #17 // Method remove:(Ljava/lang/Object;)V

  • 测试类三

    invokevirtual #10 // Method remove:(Ljava/lang/Object;)V

    invokevirtual #10 // Method remove:(Ljava/lang/Object;)V

    invokevirtual #10 // Method remove:(Ljava/lang/Object;)V

可以看出,反编译代码中都是调用实例方法的命令,所以结果中自动”向上转型”其实是jvm的功劳。jvm通过在编译时确定调用的传参类型,静态分派到具体方法的。
所以在前言中的困惑已经解除了,就是由于jvm中静态分派的实现,调用次序是int->Integer->Object。

后记

也没什么想说的,感觉在阅读源码的时候必须多想想为什么这样做,为什么要这样实现,同时通过断点或者反编译的手段找出自己的答案。keep going!

本文首发于cartoon的博客
转载请注明出处:https://cartoonyu.github.io/cartoon-blog/post/java/java的list接口的remove重载方法调用原理/

前言

在前后端交互的选择上,之前一直采用的是模板引擎(因为我只负责后端)。
而这次的一个算是作业吧,前后端都是我,所以就研究了一下JSON交互在java web的应用(主要是前端)。

优缺点

因为我是先写后端版本的,所以优缺点部分请跳转至JSON工具类的构建(后端版本)查看。

对比

因为我对js也是几天速成系列,所以框架型的jq以及ajax也是处于概念上的理解以及皮毛型的应用。
我所认识的主流的前端处理的ajax,所以就选用了ajax进行发送/接收解析json的处理。

需求拆分

因为没有打算深入学习前端,所以对需求的划分以及功能实现只限于满足这次的作业。
而我定义的功能/需求主要有两个

  • 外部函数只需进行传入接口url以及数据,接收并初步解析结果
  • 必须能进行get/post请求的发送以及接收
  • 只需要发送单实体数据

格式设计

因为是请求型报文,所以只需要形成普通的json格式

1
2
3
4
5
{
"name": "name",
"password":"password",
"account":"account"
}

代码设计

因为我希望传入接口url以及数据既能完成数据的发送与接收,而个人觉得ajax的请求响应参数中不同的只是url以及发送/接收数据不同了(限本次的需要),所以就在内部构建了一个通用的ajax的函数。

实际代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function transfer(url, data){
var result=new Array();
$.ajax({
type:"POST",
url:url,
dataType:"json",
async:false,
contentType:"application/json;charset=utf-8",
data:JSON.stringify(data),
success:function (dataReturn) {
var temp=JSON.stringify(dataReturn);
var dataObj = JSON.parse(temp);
result[0]=dataObj.status;
result[1]=dataObj.object;
}
});
return result;
}

代码思路

  1. 外部函数传入url以及数据data,data为对象类型的数据
  2. 构建ajax请求发送数据
  3. 因返回的json报文主要有两项:状态status以及数据object,所以采用数组型的返回结果
  4. 通过JSON.stringify以及JSON.parse将返回的json字符串转换成对象
  5. 根据键值拆分返回结果到3中定义的数组返回
  6. 调用函数通过result[0]判断获取操作结果,result[1]获取操作的详细数据

结果展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function clickLogin() {
var userName=$("#userName").val();
var password=$("#password").val();
var data={};
data['number']=userName;
data['password']=password;
var result=transfer("/text_project/login",data);
if(result[0]=='200'){
window.location.href="/text_project/main";
}
else{
console.log("error");
}
}

不足之处

  1. get请求的需求没有解决,曾经花了7个小时去研究解决,但是因为对js以及ajax不熟悉,知道问题所在,但是没有解决
  2. 不同结果的处理没有过多的处理
  3. 原本想构建一个统一的网络访问函数,但是由于get请求的未解决这个想法失败了

后记

因为这次时间赶,所以只是几天速成系列,只追求能用。
但是通过这次的速成,也学习到了怎么去快速掌握或满足自身需要的一些方法,也算是接触到不同方面,能在以后的JAVA后端的学习中有所对比借鉴吧。

相关链接

这是我在前台发送/接收解析json的一点经验,我另外写了一篇文章:JSON工具类的构建(后端版本),配合使用会很香哦~

本文首发于cartoon的博客
转载请注明出处:https://cartoonyu.github.io/cartoon-blog/post/json/json工具类的构建前端版本/

前言

在前后端交互的选择上,之前一直采用的是模板引擎(因为我只负责后端)。
而这次的一个算是作业吧,前后端都是我,所以就研究了一下JSON交互在java web的应用(主要是前端)。

优缺点

  • 前后端耦合

    • 模板引擎加载只是将jsp的交互方式移植到html上,前端文件格式改变了,但是jsp中前后端耦合的缺点没有改变。
    • json交互中,数据通过js/jquery动态加载在页面上,数据与页面进行分离,页面只是单纯用于展示。
  • 数据加载逻辑复用

    • 模板引擎的方式中,如果有很多相似的页面元素以及一样的数据返回格式,那只是复制粘贴大法了。
    • 在模板引擎的例子中,只需要定义一套数据加载模型,传入不同的页面元素id以及数据则能实现逻辑复用
  • 后端接口的复用
    因为我的学习路线的问题,所以我开发过安卓原生一段时间。

    • 模板引擎式的加载必须使用webView组件加载,且需另进行原生构建时接口也要另外构建。
    • json交互,谷歌爸爸鼓励使用json进行交互(一年多前的事,现在不清楚了),且安卓原生内置GJSON进行json解析与构建,所以可以在原生以及跨平台的构建有很好的平衡。

对比

既然选择了json交互的方式,而java官方据我所知是没有内置对json的支持,Spring在Controller的层面使用RestContrller注解实现对json的支持。
但是我个人强迫症很强,我对Contrller(或者说是Presenter)的定义是

  • 对用户访问的url作页面的映射
  • 对用户触发的事件进行数据的传递与返回
    正是这两点的定义,我需要在Service层组合统一格式的结果返回到上层,所以需要第三方json支持。
    可选择的JSON库有很多,GSON,FastJson,Jackson,根据对比,我选择为马老师充值一波。

需求拆分

我初步定义的需求主要有三个

  • Service传递统一处理结果到上层
  • 无论是单一数据实体或者List型数据处理的结果是一样的
  • 处理过程是独立的,不依赖于实体类的支持

格式设计

在网上看过很多后端返回数据的格式,很多都是返回一个处理的status以及具体的数据,而这个status是根据http状态码进行设定的,因为这次时间比较紧,所以我就采用了这个方案。

1
2
3
4
5
6
{
"status": "status",
"object": {

}
}

代码设计

因为我希望Controller能直接拿到结果,所以构建结果的过程全放在Result类中。
而结果构建我主要分为两种:只有状态码(通知处理结果)以及具有返回结果(数据显示),而根据结果的个人也分为两种:单个数据以及List型数据。

实际代码

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
@Getter
@Slf4j
public class ResultSet {

private JSONObject result;

public ResultSet(){
result=new JSONObject();
}

/**
* 初始化状态码
* @param status
*/
public void initStatus(String status){
result.put("status", status);
}

/**
* 初始化状态码以及返回数据
* @param status
* @param obj
*/
public void initData(String status,Object obj){
initStatus(status);
if(obj instanceof List){
List list=(List)obj;
JSONArray array=new JSONArray();
for(Object object:list){
array.add(putObjectToJSON(object));
}
result.put("object",array);
}
else {
result.put("object",putObjectToJSON(obj));
}
}

/**
* 将单个Object放入json文件中
* @param obj
* @return
*/
private JSONObject putObjectToJSON(Object obj){
JSONObject result=new JSONObject();
Field[] fields=obj.getClass().getDeclaredFields();
for(Field field:fields){
field.setAccessible(true);
String fieldName=field.getType().getSimpleName();
if(fieldName.equals("Department")||fieldName.equals("Job")){
JSONObject tempJson=new JSONObject();
try {
Object tempObject = field.get(obj);
Field[] tempFields=tempObject.getClass().getDeclaredFields();
for(Field tempField:tempFields){
tempField.setAccessible(true);
tempJson.put(tempField.getName(),tempField.get(tempObject));
tempField.setAccessible(false);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
if(fieldName.equals("Department")){
result.put("depart",tempJson);
}
else {
result.put("job",tempJson);
}
}
else {
try {
result.put(field.getName(),field.get(obj));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
field.setAccessible(false);
}
return result;
}
}

代码思路

  1. Service通过initStatus/initData传入数据/状态码进行对象的生成
  2. Controller通过result的getter方法获取处理结果
  3. 单个数据以及List数据的处理
    1. 单个对象直接通过putObjectToJSON进行处理
    2. List数据通过对象类型判断,向下转型,遍历元素形成JSONArray进行处理,对元素处理的方法也是采用putObjectToJSON
  4. 嵌套对象的处理
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class Employee {

    private int id;

    private Department depart;

    private Job job;

    private String name;

    ..................
    }
    1. 通过反射获取对象的所有成员变量类型以及对应的值
    2. 遇到上述bean类含有嵌套自定义对象时,递归生成json文件加入到结果json中

结果演示

​ (左为单object型,右为List型)

不足之处

  1. 状态码的设置应该采用枚举类的赋值,能更好的约束返回的状态码
  2. 在对象转换方面,应该采用配置扫描的方式。在配置文件中写入bean的包所在,在嵌套对象转换时通过扫描配置文件的信息判断
  3. 在嵌套对象的转换方面,只是做了一层的嵌套转换,更多层的没有考虑到,之后会再重构的
  4. 对异常处理方面,只是简单的输出错误信息。应该对错误信息进行进一步的处理

后记

因为这次时间比较赶,从项目的立项到成品的建立花了5天时间,所以注意到很多细节,但是没有去处理。
有想过之后有时间的话将这个工具类的细节完善起来,形成jar包供自己或者供开源。

相关连接

这只是后台对结果的统一处理,我另外写了一篇文章:JSON工具类的构建(前端版本),配合使用效果更佳哦~

本文首发于cartoon的博客
转载请注明出处:https://cartoonyu.github.io/cartoon-blog/post/json/json工具类的构建后端版本/

前言

​ 在研究java集合源码的时候,发现了一个很少用但是很有趣的点:Queue以及Deque,平常在写leetcode经常用LinkedList向上转型Deque作为栈或者队列使用,但是一直都不知道Queue的作用,于是就直接官方文档好了。

正文

概念

从上图看出,Queue以及Deque都是继承于Collection,Deque是Queue的子接口。

下面来看一下官方文档的解释。

A linear collection that supports element insertion and removal at both ends. The name deque is short for “double ended queue” and is usually pronounced “deck”. Most Deque implementations place no fixed limits on the number of elements they may contain, but this interface supports capacity-restricted deques as well as those with no fixed size limit.

A collection designed for holding elements prior to processing. Besides basic Collection operations, queues provide additional insertion, extraction, and inspection operations. Each of these methods exists in two forms: one throws an exception if the operation fails, the other returns a special value (either null or false, depending on the operation). The latter form of the insert operation is designed specifically for use with capacity-restricted Queue implementations; in most implementations, insert operations cannot fail.

从Deque的解释中,我们可以得知:Deque是double ended queue,我将其理解成双端结束的队列,双端队列,可以在首尾插入或删除元素。而Queue的解释中,Queue就是简单的FIFO队列。

所以在概念上来说,Queue是FIFO的单端队列,Deque是双端队列。

而在使用上,又有什么差别呢?

使用

从上图我们可以得知,Queue有一个直接子类PriorityQueue,而Deque中直接子类有两个:LinkedList以及ArrayDeque。

  • PriorityQueue

我觉得重点就在圈定的两个单词:无边界的,优先级的堆。然后再看看源码

在第一张图片的源码中,明显看到PriorityQueue的底层数据结构是数组,而无边界的形容,那么指明了PriorityQueue是自带扩容机制的,具体请看PriorityQueue的grow方法。

在第二张第三张图片中,可以看到插入元素的时候是需要经过compareTo的处理,那么最常用就是一些范围极值的输出,类似于堆排序的用法。

下面演示一下正反序输出三个元素的使用

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
private static void negativePrint(int[] nums) {
PriorityQueue<Integer> queue=new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1;
}
});
for(int temp:nums){
queue.add(temp);
}
System.out.println();
System.out.print("倒序输出:");
for(int i=0;i<3;i++){
System.out.print(queue.poll()+" ");
}
}

private static void positivePrint(int[] nums){
PriorityQueue<Integer> queue=new PriorityQueue<>();
for(int temp:nums){
queue.add(temp);
}
System.out.print("正序输出:");
for(int i=0;i<3;i++){
System.out.print(queue.poll()+" ");
}
}
1
2
正序输出:1 2 3 
倒序输出:9 8 8

这个在一些排行榜或者输入第N个最大/小元素会比较常用。

  • LinkedList以及ArrayDeque

从官方解释来看,ArrayDeque是无初始容量的双端队列,LinkedList则是双向链表。而我们还能看到,ArrayDeque作为队列时的效率比LinkedList要高,而在栈的使用场景下,无疑具有尾结点不需判空的LinkedList较高效。

下面演示ArrayDeque作为队列以及LinkedList作为栈的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static void usingAsQueue() {
Deque<Integer> queue=new ArrayDeque<>();
System.out.println("队列为空:"+queue.isEmpty()); //判断队列是否为空
queue.addLast(12); //添加元素
System.out.println("队列为空:"+queue.isEmpty()); //判断队列是否为空
System.out.println(queue.peekFirst()); //获取队列首部元素
System.out.println(queue.pollFirst()); //获取并移除栈顶元素
System.out.println("队列为空:"+queue.isEmpty()); //判断队列是否为空
}

private static void usingAsStack() {
//作为栈使用
Deque<Integer> stack=new LinkedList<>();
System.out.println("栈为空:"+stack.isEmpty()); //判断栈是否为空
stack.addFirst(12);
System.out.println("栈为空:"+stack.isEmpty()); //判断栈是否为空
System.out.println(stack.peekFirst()); //获取栈顶元素
System.out.println(stack.pollFirst()); //获取并移除栈顶元素
System.out.println("栈为空:"+stack.isEmpty()); //判断栈是否为空
System.out.println("============================================");
}

栈为空:true
栈为空:false
12
12

栈为空:true

队列为空:true
队列为空:false
12
12
队列为空:true

小提示

在Deque中,获取并移除元素的方法有两个,分别是removeXxx以及peekXxx。

存在元素时,两者的处理都是一样的。但是当Deque内为空时,removeXxx会直接抛出NoSuchElementException,而peekXxx则会返回null。

所以无论在实际开发或者算法时,推荐使用peekXxx方法

其实ArrayDeque和LinkedList都可以作为栈以及队列使用,但是从执行效率来说,ArrayDeque作为队列以及LinkedList作为栈使用会是更好的选择。

另外,我在leetcode看到有人采用Vector下的Stack,这个同步加锁粒度过大(对象级),另外我觉得算法中没有线程同步的需要吧。

  • 小结

PriorityQueue可以作为堆使用,而且可以根据传入的Comparator实现大小的调整,会是一个很好的选择。

ArrayDeque通常作为栈或队列使用,但是栈的效率不如LinkedList高。

LinkedList通常作为栈或队列使用,但是队列的效率不如ArrayQueue高。

总结

在java中,Queue被定义成单端队列使用,Deque被定义成双端队列使用。

而由于双端队列的定义,Deque可以作为栈或者队列使用,而Queue只能作为队列或者依赖于子类的实现作为堆使用。

本文首发于cartoon的博客
转载请注明出处:https://cartoonyu.github.io/cartoon-blog/post/java/queue%E4%B8%8Edeque%E7%9A%84%E5%8C%BA%E5%88%AB/

前序

在看HashMap源码的时候,看到HashMap的hash函数里面有用到>>>的运算符,之前经常在除2操作用到>>运算符,但是还是第一次看到>>>,于是就来记录一下。

情景复现

hashMap的hash函数源码

hashMap的hash函数源码

因为里面主要是先获取key的hashCode,这是jvm生成的,所以我单独用1模拟hashCode

1
System.out.println((h = 1) ^ (h >>> 16));

结果如下

1
情景复现: 1

步骤解析

这段代码主要由三段代码运算而成

  1. h直接赋值为1
  2. h>>>16位
  3. 步骤1与步骤2的异或运算

所以从结果推断出1>>>16结果为0

测试

1
2
3
4
System.out.println("-12>>>2结果为:"+Integer.toBinaryString(-12>>>2));
System.out.println("12>>>2结果为:"+Integer.toBinaryString(12>>>2));
System.out.println("-12>>2结果为:"+Integer.toBinaryString(-12>>2));
System.out.println("12>>2结果为:"+Integer.toBinaryString(12>>2));
1
2
3
4
-12>>>2结果为:111111111111111111111111111101
12>>>2结果为:11
-12>>2结果为:11111111111111111111111111111101
12>>2结果为:11

分析

从结果可以看出,利用正数做操作时,>>与>>>结果没有变化,但是负数的操作时发生了变化,这个对比证明了>>>的操作与符号位有关。

  • 正数操作

由于位运算时,利用Integer.toBinaryString方法输出不会输出前置0,所以可以推断两个运算符的操作都是右移n位补0

  • 负数操作

由于负数存储的是它的补码,在进行>>运算的时候明显看到生成的二进制字符串补1,且长度比正数运算的时候要长,java在二进制中是不区分符号位的,所以最后的十进制表示的数会异常大。

总结

两个运算符,在进行正数移位的时候操作是一样的。但是在处理负数时,>>补1,是带符号操作的。而>>>补0,是无符号操作的。

&nbsp;&nbsp;&nbsp;&nbsp;本文首发于cartoon的博客

&nbsp;&nbsp;&nbsp;&nbsp;转载请注明出处:https://cartoonyu.github.io/cartoon-blog/post/java/无符号运算符与有符号运算符的区别/

&nbsp;&nbsp;&nbsp;&nbsp;本文首发于cartoon的博客
&nbsp;&nbsp;&nbsp;&nbsp;转载请注明出处:https://cartoonyu.github.io/cartoon-blog/post/java/for与while时间的对比

&nbsp;&nbsp;&nbsp;&nbsp;相关文章:JAVA遍历机制的性能的比较

前言

索引随机访问数组相信是很常见的操作.

但是昨天在做leetcode的Reverse String时,发现了很奇怪的现象,具体如下图

当时我也觉得不可思议,怎么快了那么多,所以今天复盘一下。

正文

注:这篇文章只涉及原始数组的索引遍历,不涉及包装数据结构以及foreach

测试代码
  • for
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static void textFor(){
int[] data=new int[1000];
int i=0;
for(;i<1000;i++){
data[i]=i;
}
i=0;
long start=System.currentTimeMillis();
for(;i<1000;i++){
System.out.print(data[i]+" ");
}
long end=System.currentTimeMillis();
System.out.println();
System.out.println("for use:"+(end-start)+"ms");
}
  • while
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static void textWhile(){
int[] data=new int[1000];
int i=0;
for(;i<1000;i++){
data[i]=i;
}
i=0;
long start=System.currentTimeMillis();
while(i<1000){
System.out.print(data[i++]+" ");
}
long end=System.currentTimeMillis();
System.out.println();
System.out.println("while use:"+(end-start)+"ms");
}
结果
1
2
3
4
5
6
7
8
9
10
11
for use:35ms
while use:15ms

for use:14ms
while use:6ms

for use:14ms
while use:8ms

for use:20ms
while use:5ms

所用时间可能不一样,但是大概比例应该跟我的差不多

有点意外的是,while比for竟然要少一倍(大概)的时间,颠覆了我之前的认知。

结果分析

虽然我没有debug代码,但是我猜测是循环执行语句的多少差别。

for中,执行顺序是

  • 判断循环变量是否越界
  • 执行打印语句
  • 循环变量自增

while中,执行顺序是

  • 判断循环变量是否越界
  • 执行打印语句,循环变量自增

与for相比,while所执行的语句量少掉1/3,所以我觉得这就是原因。(如果有更好的原因可以评论或者发起Issue)

后话

生命不息,技术不止。

很多时候我也为了代码量的减少不理会运行时间的差异,这次吸收教训,之后在实际开发会更加注意时间。

&nbsp;&nbsp;&nbsp;&nbsp;本文首发于cartoon的博客

&nbsp;&nbsp;&nbsp;&nbsp;转载请注明出处:https://cartoonyu.github.io/cartoon-blog/post/java/object%E7%9A%84%E6%88%90%E5%91%98%E6%96%B9%E6%B3%95%E4%BB%A5%E5%8F%8A%E4%BD%9C%E7%94%A8/

前言

对的。这次也是面试题,又是有点懵逼的一道题,记得当时只答出了wait跟notify。。。

正文

学java的都知道,Object是所有类的父类,但是相信很多人都忽略掉Object中的成员方法(包括我)。

翻过官方文档后,发现其实Object类成员方法可以总结为以下几类

&nbsp; 方法名 作用 注意
多线程操作 wait 线程等待,线程进入阻塞状态 /
notify/notifyAll 唤醒线程,线程回到就绪状态 /
垃圾回收 finalize 通知垃圾收集器回收对象 只是提醒,回收时间仍由垃圾收集器决定
对象克隆 clone 克隆对象 protected方法,不能被直接调用,若想实现克隆通过实现Cloneable重写clone方法实现
对象比较 hashCode/equals 判断对象时候相等 hashCode相等,equals不一定相等;equals相等,hashCode一定相等
获取对象信息 getClass 获取对象所属类 在反射中比较常用

后话

其实Object类的很多方法都很实用,多线程同步,对象比较等等,但是平时自己比较少关注,可能是我菜鸡吧。

希望看到文章的你们能有所收获,也希望我以后被问到这个不会再懵逼。

&nbsp;&nbsp;&nbsp;&nbsp;本文首发于cartoon的博客
&nbsp;&nbsp;&nbsp;&nbsp;转载请注明出处:https://cartoonyu.github.io/cartoon-blog/post/java/java实现克隆的方法

前言

这也是昨天的面试题。

当时只说了深拷贝以及浅拷贝,面试官问了两遍还有吗,我很肯定的说就这两种了,面试结束之后查了一下,啪啪打脸。

正文

JAVA实现克隆有两种形式

  • 浅克隆
  • 深克隆
浅克隆与深克隆的区别

JAVA将数据类型分为基本数据类型以及引用数据类型,我认为浅克隆与深克隆的区别主要在于对引用类型的成员属性的操作。深度克隆应该递归克隆引用类型的成员属性。

浅克隆实现
  • 实现Cloneable接口
  • 重写clone方法,调用父类的clone方法

代码

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
58
59
60
61
public class Text implements Cloneable{

private int age;

private Name name;

public int getAge() {
return age;
}

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

public Name getName() {
return name;
}

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

@Override
protected Object clone(){
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}

class Name{
private String name;

public String getName() {
return name;
}

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

public class Main {

public static void main(String[] args){
Name name1=new Name();
name1.setName("name1");
Text t1=new Text();
t1.setAge(12);
t1.setName(name1);
Text t2=(Text) t1.clone();
System.out.println(t2.getName().getName());
name1.setName("name2");
System.out.println(t2.getName().getName());

}

}

输出

1
2
name1
name2

结果分析

因为只是直接调用父类的clone方法,没有对成员属性进行处理,所以在修改t1属性name的值时,t2属性name的值也会随之改变。

优点

简单易实现

缺点

无法真正克隆对象

深克隆实现
通过递归克隆实现

代码

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
public class Text implements Cloneable{

private int age;

private Name name;

public int getAge() {
return age;
}

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

public Name getName() {
return name;
}

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

@Override
protected Object clone(){
Text text=null;
try {
text=(Text) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
text.setName((Name) text.getName().clone());
return text;
}
}

class Name implements Cloneable{
private String name;

public String getName() {
return name;
}

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

@Override
protected Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}

输出

1
2
name1
name1
通过序列化实现

代码

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
58
59
60
61
public class Text implements Serializable{

private static final long serialVersionUID = 8723901148964L;

private int age;

private Name name;

public int getAge() {
return age;
}

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

public Name getName() {
return name;
}

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

public Object myClone(){
Text text=null;
ByteArrayOutputStream bos=new ByteArrayOutputStream();
try {
ObjectOutputStream oos=new ObjectOutputStream(bos);
oos.writeObject(this);
ByteArrayInputStream bis=new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois=new ObjectInputStream(bis);
text=(Text)ois.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return text;
}
}

class Name implements Serializable {

private static final long serialVersionUID = 872390113109L;

private String name;

public String getName() {
return name;
}

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

@Override
public String toString() {
return name;
}
}

输出

1
2
name1
name1

结果分析

采用深克隆能有效隔离源对象与克隆对象的联系。

从实现过程来说,递归克隆存在克隆过程多且复杂的缺点,所以建议采用序列化的方式进行

深克隆。

总结

JAVA对象克隆共有两种形式,三种方法

  • 浅克隆
    • 调用clone方法
  • 深克隆
    • 递归调用clone方法
    • 序列化对象

三种方法之间互有优缺点,具体采用要根据实际情况。

&nbsp;&nbsp;&nbsp;&nbsp;本文首发于cartoon的博客
&nbsp;&nbsp;&nbsp;&nbsp;转载请注明出处:https://cartoonyu.github.io/cartoon-blog/post/java/collections/

&nbsp;&nbsp;&nbsp;&nbsp;推荐一篇不错的博文:https://gitee.com/SnailClimb/JavaGuide/blob/master/docs/java/Basis/Arrays,CollectionsCommonMethods.md

前言

就在刚刚面试,被问到了Collections工具类有什么功能,我懵了,很少涉及到Collections这个工具类,只答了对集合元素的操作以及将线程不同步的集合类转换成线程同步,下定决心复盘一下。

正文

Collections工具类是java.util包中的一个工具类,主要功能是对集合及其元素进行操作。虽然被问的有点懵,在结束查看官方文档时发现其实可以分成以下几大部分

对集合本体的操作
线程同步集合的包装
  • 特征
    • 方法名为synchronizedXxx
  • 适用范围
    • List
    • Collection
    • Set
    • Map
  • 缺点
    • 每次读写都要加锁,锁的层级为对象,不利于多线程的同时操作
    • 在使用Iterator的遍历时修改元素ConcurrentModificationException
    • 建议使用java.util.concurrent的集合线程同步类
返回不可变集合
  • 特征

    • 方法名为emptyXxx(空集合)/singletonXxx(包含传入元素的集合)/unmodifiableXxx(包含传入集合元素的集合)
  • 适用范围

    • emptyXxx

      • Set
      • List
      • Map
      • Iterator
      • Enumeration
    • singletonXxx

      • Set
      • List
      • Map
    • unmodifiableXxx

      • Map

      • List

      • Set

返回指定集的动态类型安全视图
  • 特征
    • 方法名为checkedXxx
  • 适用范围
    • List
    • Map
    • Queue
    • Set
    • Collection
集合间的转换
  • 特征

    • asLifoQueue(将传入的Deque转换成Queue)

    • list(将传入的Enumeration转换成ArrayList)

    • newSetFromMap(根据传入的空Map返回Set)

    • nCopies(根据传入的n返回含n个副本的List)

集合内元素的操作
添加元素到集合中
  • 特征
    • addAll
    • copy(将源集合元素复制到目标集合中)
  • 适用范围
    • addAll
      • Collection
    • copy
      • List
查找元素
  • 特征
    • binarySearch(二分查找特定元素)
    • frequency(查找元素出现次数)
    • indexOfSubList(返回目标list在源list的开始位置)
    • subIndexOfSubList(返回目标list在源list的结束位置)
    • shuffle(返回随机索引元素)
  • 适用范围
    • binarySearch
      • List
    • frequency
      • Collection
    • shuffle
      • List
替换
  • 特征
    • fill(替换集合所有元素)
    • replaceAll(替换特定的值)
  • 适用范围
    • fill
      • List
    • replaceAll
      • List
改变元素位置
  • 特征
    • sort(排序)
    • swap
    • rotate(反转)
    • reverse
  • 适用范围
    • List
对比元素
  • 特征
    • min/max(寻找最大/小元素)
    • disJoint(判断两个集合元素是否全不同)
  • 适用范围
    • Collection

总结

Collections工具类能对各接口以及实现类实现多种操作

  • 集合类级操作
    • 返回线程安全集合
    • 返回不可变集合
    • 返回安全视图
    • 集合间的转换
  • 涉及到内部元素的操作
    • 添加元素到集合中
    • 查找特定元素
    • 替换元素
    • 改变元素位置
    • 元素间的比较

虽然有些方法不如其他包内的工具类好用,但是总体来说功能还是非常强大的。

这篇文章算是对官方文档的总结和归纳,也加以自己的思考,也是面试题之一,希望自己能在之后不会再吃这道题的亏。

&nbsp;&nbsp;&nbsp;&nbsp;本文首发于cartoon的博客

&nbsp;&nbsp;&nbsp;&nbsp;转载请注明出处:https://cartoonyu.github.io/cartoon-blog/post/java/java%E9%81%8D%E5%8E%86%E6%9C%BA%E5%88%B6%E7%9A%84%E6%80%A7%E8%83%BD%E6%AF%94%E8%BE%83/

缘由

&nbsp;&nbsp;&nbsp;&nbsp;近段时间在写leetcode的Lemonade Change时候,发现了for循环与forEach循环的耗时是不一致的,在提交记录上面差了一倍……

&nbsp;&nbsp;&nbsp;&nbsp;平常开发绝大部分业务逻辑的实现都需要遍历机制的帮忙,虽说也有注意到各数据结构操作的性能比较,但是忽视了遍历机制性能的差异。原本前两天就开始动手写,拖延症……

正文

&nbsp;&nbsp;&nbsp;&nbsp;现阶段我所知道JAVA遍历机制有三种

  • for循环

  • forEach循环

  • Iterator循环

&nbsp;&nbsp;&nbsp;&nbsp;JAVA数据结构千千万,但是大部分都是对基础数据结构的封装,比较HashMap依赖于Node数组,LinkedList底层是链表,ArrayList对数组的再封装……扯远了

&nbsp;&nbsp;&nbsp;&nbsp;总结来说,JAVA的基础数据结构,我觉得有两种

  • 数组
  • 链表

&nbsp;&nbsp;&nbsp;&nbsp;如果是加上Hash(Hash的操作与数组以及链表不太一致),就是三种

&nbsp;&nbsp;&nbsp;&nbsp;因为平常开发大部分都优先选择包装后的数据结构,所以下面我会使用

  • ArrayList(包装后的数组)
  • LinkedList(包装后的链表)
  • HashSet(包装后的Hash类型数组)

&nbsp;&nbsp;&nbsp;&nbsp;这三种数据结构在遍历机制不同的时候时间的差异

&nbsp;&nbsp;&nbsp;&nbsp;可能有人对我为什么不对比HashMap呢,因为JAVA设计中,是先实现了Map,再实现Set。如果你有阅读过源码就会发现:每个Set子类的实现中,都有一个序列化后的Map对应属性实现,而因为Hash的查找时间复杂度为O(1),得出key后查找value的时间大致是一致的,所以我不对比HashMap。

题外话

&nbsp;&nbsp;&nbsp;&nbsp;我在阅读《疯狂JAVA》读到:JAVA的设计者将Map的内部entry数组中的value设为null进而实现了Set。因为我是以源码以及官方文档为准,具体我不清楚正确与否,但是因为Hash中的key互不相同,Set中元素也互不相同,所以我认为这个观点是正确的。

&nbsp;&nbsp;&nbsp;&nbsp;为了测试的公平性,我会采取以下的限定

  • 每种数据结构的大小都设置三种量级
    • 10
    • 100
    • 1000
  • 元素都采用随机数生成
  • 遍历进行操作都为输出当前元素的值

&nbsp;&nbsp;&nbsp;&nbsp;注:时间开销受本地环境的影响,可能测量值会出现变化,但是总体上比例是正确的

ArrayList的比较

  • 代码

    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
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    public class TextArray {

    private static Random random;

    private static List<Integer> list1;

    private static List<Integer> list2;

    private static List<Integer> list3;

    public static void execute(){
    random=new Random();
    initArray();
    testForWith10Object();
    testForEachWith10Object();
    testIteratorWith10Object();
    testForWith100Object();
    testForEachWith100Object();
    testIteratorWith100Object();
    testForWith1000Object();
    testForEachWith1000Object();
    testIteratorWith1000Object();
    }

    private static void testForWith10Object(){
    printFor(list1);
    }

    private static void testForWith100Object(){
    printFor(list2);
    }

    private static void testForWith1000Object(){
    printFor(list3);
    }

    private static void testForEachWith10Object(){
    printForeach(list1);
    }

    private static void testForEachWith100Object(){
    printForeach(list2);
    }

    private static void testForEachWith1000Object(){
    printForeach(list3);
    }

    private static void testIteratorWith10Object() {
    printIterator(list1);
    }

    private static void testIteratorWith100Object() {
    printIterator(list2);
    }

    private static void testIteratorWith1000Object() {
    printIterator(list3);
    }

    private static void printFor(List<Integer> list){
    System.out.println();
    System.out.print("data:");
    long start=System.currentTimeMillis();
    for(int i=0,length=list.size();i<length;i++){
    System.out.print(list.get(i)+" ");
    }
    System.out.println();
    long end=System.currentTimeMillis();
    System.out.println("for for "+list.size()+":"+(end-start)+"ms");
    }

    private static void printForeach(List<Integer> list){
    System.out.println();
    System.out.print("data:");
    long start=System.currentTimeMillis();
    for(int temp:list){
    System.out.print(temp+" ");
    }
    System.out.println();
    long end=System.currentTimeMillis();
    System.out.println("foreach for "+list.size()+":"+(end-start)+"ms");
    }

    private static void printIterator(List<Integer> list){
    System.out.println();
    System.out.print("data:");
    Iterator<Integer> it=list.iterator();
    long start=System.currentTimeMillis();
    while(it.hasNext()){
    System.out.print(it.next()+" ");
    }
    System.out.println();
    long end=System.currentTimeMillis();
    System.out.println("iterator for "+list.size()+":"+(end-start)+"ms");
    }

    private static void initArray(){
    list1=new ArrayList<>();
    list2=new ArrayList<>();
    list3=new ArrayList<>();
    for(int i=0;i<10;i++){
    list1.add(random.nextInt());
    }
    for(int i=0;i<100;i++){
    list2.add(random.nextInt());
    }
    for(int i=0;i<1000;i++){
    list3.add(random.nextInt());
    }
    }
    }
  • 输出(忽略对元素的输出)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    for for 10:1ms
    foreach for 10:0ms
    iterator for 10:2ms

    for for 100:5ms
    foreach for 100:4ms
    iterator for 100:12ms

    for for 1000:33ms
    foreach for 1000:7ms
    iterator for 1000:16ms
    10 100 1000
    for 1ms 5ms 33ms
    forEach 0ms 4ms 7ms
    Iterator 2ms 12ms 16ms
  • 结论

    &nbsp;&nbsp;&nbsp;&nbsp;for的性能最不稳定,foreach次之,Iterator最好

  • 使用建议

    1. 在数据量不明确的情况下(可能1w,10w或其他),建议使用Iterator进行遍历

    2. 在数据量明确且量级小的时候,优先使用foreach

    3. 需要使用索引时,使用递增变量的开销比for的要小

LinkedList的比较

  • 代码

    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
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    public class TextLinkedList {

    private static Random random;

    private static List<Integer> list1;

    private static List<Integer> list2;

    private static List<Integer> list3;

    public static void execute(){
    random=new Random();
    initList();
    testForWith10Object();
    testForEachWith10Object();
    testIteratorWith10Object();
    testForWith100Object();
    testForEachWith100Object();
    testIteratorWith100Object();
    testForWith1000Object();
    testForEachWith1000Object();
    testIteratorWith1000Object();
    }

    private static void testForWith10Object() {
    printFor(list1);
    }

    private static void testForEachWith10Object() {
    printForeach(list1);
    }

    private static void testIteratorWith10Object() {
    printIterator(list1);
    }

    private static void testForWith100Object() {
    printFor(list2);
    }

    private static void testForEachWith100Object() {
    printForeach(list2);
    }

    private static void testIteratorWith100Object() {
    printIterator(list2);
    }

    private static void testForWith1000Object() {
    printFor(list3);
    }

    private static void testForEachWith1000Object() {
    printForeach(list3);
    }

    private static void testIteratorWith1000Object() {
    printIterator(list3);
    }

    private static void printFor(List<Integer> list){
    System.out.println();
    System.out.print("data:");
    long start=System.currentTimeMillis();
    for(int i=0,size=list.size();i<size;i++){
    System.out.print(list.get(i));
    }
    System.out.println();
    long end=System.currentTimeMillis();
    System.out.println("for for "+list.size()+":"+(end-start)+"ms");
    }

    private static void printForeach(List<Integer> list){
    System.out.println();
    System.out.print("data:");
    long start=System.currentTimeMillis();
    for(int temp:list){
    System.out.print(temp+" ");
    }
    System.out.println();
    long end=System.currentTimeMillis();
    System.out.println("foreach for "+list.size()+":"+(end-start)+"ms");
    }

    private static void printIterator(List<Integer> list){
    System.out.println();
    System.out.print("data:");
    Iterator<Integer> it=list.iterator();
    long start=System.currentTimeMillis();
    while(it.hasNext()){
    System.out.print(it.next()+" ");
    }
    System.out.println();
    long end=System.currentTimeMillis();
    System.out.println("iterator for "+list.size()+":"+(end-start)+"ms");
    }


    private static void initList() {
    list1=new LinkedList<>();
    list2=new LinkedList<>();
    list3=new LinkedList<>();
    for(int i=0;i<10;i++){
    list1.add(random.nextInt());
    }
    for(int i=0;i<100;i++){
    list2.add(random.nextInt());
    }
    for(int i=0;i<1000;i++){
    list3.add(random.nextInt());
    }
    }
    }
  • 输出(忽略对元素的输出)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    for for 10:0ms
    foreach for 10:1ms
    iterator for 10:0ms

    for for 100:1ms
    foreach for 100:0ms
    iterator for 100:3ms

    for for 1000:23ms
    foreach for 1000:25ms
    iterator for 1000:4ms
    10 100 1000
    for 0ms 1ms 23ms
    forEach 1ms 0ms 25ms
    Iterator 0ms 3ms 4ms
  • 结论

    &nbsp;&nbsp;&nbsp;&nbsp;foreach的性能最不稳定,for次之,Iterator最好

  • 使用建议

    1. 尽量使用Iterator进行遍历

    2. 需要使用索引时,使用递增变量的开销比for的要小

HashSet的比较

&nbsp;&nbsp;&nbsp;&nbsp;注:因Hash遍历算法与其他类型不一致,所以取消了for循环的比较

  • 代码

    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
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    public class TextHash {

    private static Random random;

    private static Set<Integer> set1;

    private static Set<Integer> set2;

    private static Set<Integer> set3;

    public static void execute(){
    random=new Random();
    initHash();
    testIteratorWith10Object();
    testForEachWith10Object();
    testIteratorWith100Object();
    testForEachWith100Object();
    testIteratorWith1000Object();
    testForEachWith1000Object();
    }

    private static void testIteratorWith10Object() {
    printIterator(set1);
    }

    private static void testForEachWith10Object() {
    printForeach(set1);
    }

    private static void testIteratorWith100Object() {
    printIterator(set2);
    }

    private static void testForEachWith100Object() {
    printForeach(set2);
    }

    private static void testIteratorWith1000Object() {
    printIterator(set3);
    }

    private static void testForEachWith1000Object() {
    printForeach(set3);
    }

    private static void initHash() {
    set1=new HashSet<>();
    set2=new HashSet<>();
    set3=new HashSet<>();
    for(int i=0;i<10;i++){
    set1.add(random.nextInt());
    }
    for(int i=0;i<100;i++){
    set2.add(random.nextInt());
    }
    for(int i=0;i<1000;i++){
    set3.add(random.nextInt());
    }
    }

    private static void printIterator(Set<Integer> data){
    System.out.println();
    System.out.print("data:");
    long start=System.currentTimeMillis();
    Iterator<Integer> it=data.iterator();
    while (it.hasNext()){
    System.out.print(it.next()+" ");
    }
    System.out.println();
    long end=System.currentTimeMillis();
    System.out.println("iterator for "+data.size()+":"+(end-start)+"ms");
    }

    private static void printForeach(Set<Integer> data){
    System.out.println();
    System.out.print("data:");
    long start=System.currentTimeMillis();
    for(int temp:data){
    System.out.print(temp+" ");
    }
    System.out.println();
    long end=System.currentTimeMillis();
    System.out.println("foreach for "+data.size()+":"+(end-start)+"ms");
    }
    }
  • 输出(忽略对元素的输出)

    1
    2
    3
    4
    5
    6
    7
    8
    iterator for 10:0ms
    foreach for 10:0ms

    iterator for 100:6ms
    foreach for 100:0ms

    iterator for 1000:30ms
    foreach for 1000:9ms
    10 100 1000
    foreach 0ms 0ms 9ms
    Iterator 0ms 6ms 30ms
  • 结论

    &nbsp;&nbsp;&nbsp;&nbsp;foreach性能遥遥领先于Iterator

  • 使用建议

    &nbsp;&nbsp;&nbsp;&nbsp;以后就选foreach了,性能好,写起来也方便。

总结

  1. for循环性能在三者的对比中总体落于下风,而且开销递增幅度较大。以后即使在需要使用索引时我宁愿使用递增变量也不会使用for了。
  2. Iterator的性能在数组以及链表的表现都是最好的,应该是JAVA的设计者优化过了。在响应时间敏感的情况下(例如web响应),优先考虑。
  3. foreach的性能属于两者之间,写法简单,时间不敏感的情况下我会尽量选用。

&nbsp;&nbsp;&nbsp;&nbsp;以上就是我对常见数据结构遍历机制的一点比较,虽然只是很初步,但是从中我也学到了很多东西,希望你们也有所收获。

&nbsp;&nbsp;&nbsp;&nbsp;如果你喜欢本文章,可以收藏阅读,如果你对我的其他文章感兴趣,欢迎到我的博客查看。