diff --git a/README.md b/README.md index ad23d2a4b3f5d05a300ac904fdad1f795694efda..a6c76d80a55d7ac26e952e74a2def4e62033c37b 100644 --- a/README.md +++ b/README.md @@ -15,16 +15,10 @@
- - - - # 前言 同学们好,我是二哥呀,欢迎来到《教妹学 Java》专栏。 -[我妹今年上大学了](https://mp.weixin.qq.com/s/bsu9uH8VKh5Vtue-9SafwQ),学的计算机编程,立志像我一样做一名正儿八经的 Java 程序员。我期初是反抗的,因为程序员这行业容易掉头发,作为一名需要美貌的女生,长发飘飘是必须的啊。但与其反抗,不如做点更积极的事情,比如说写点有趣的文章,教她更快地掌握 Java 这门编程语言,于是就有了这个专栏。 - ![](https://cdn.jsdelivr.net/gh/itwanger/Tech-Sister-Learn-Java/images/tech-sister-01.png) @@ -59,12 +53,31 @@ - [代码初始化块](docs/object-class/code-init.md) - [final 关键字](docs/object-class/java-final.md) - [instanceof](docs/object-class/java-instanceof.md) +- [抽象类](docs/object-class/java-abstract.md) +- [接口](docs/object-class/java-interface.md) +- [值传递与引用传递](docs/object-class/pass-by-value.md) +- [浅拷贝与深拷贝](docs/object-class/deep-copy.md) +- [自动拆箱与装箱](docs/object-class/box.md) + +## **数组** + +- [数组概览](docs/array/gailan.md) +- [Arrays](docs/array/arrays.md) +- [打印数组](docs/array/print.md) + +## **字符串** + +- [字符串源码分析](docs/string/source.md) +- [字符串常量池](docs/string/constant-pool.md) +- [intern](docs/string/intern.md) + +## **字符串** # 公众号 如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号“**沉默王二**”。 -**《教妹学Java》:** 本文档的离线版 PDF 版可以扫描下方的二维码关注我的[公众号](#公众号)后回复 **" 03"** 关键字即可领取! +**《教妹学Java》:** 本文档的离线版 PDF 版可以扫描下方的二维码关注我的[公众号](#公众号)后回复 **"03"** 关键字即可领取!
@@ -73,7 +86,7 @@ # Donate -开源不易,如果《教妹学 Java》专栏对你有些帮助,可以请作者喝杯咖啡,让他继续肝! +开源不易,如果《教妹学 Java》专栏对你有些帮助,可以请二哥喝杯咖啡,让他继续肝!
diff --git a/docs/array/arrays.md b/docs/array/arrays.md new file mode 100644 index 0000000000000000000000000000000000000000..3f924e3f0ece6a12824fb36bd88425899e53bcc5 --- /dev/null +++ b/docs/array/arrays.md @@ -0,0 +1,416 @@ +## 数组专用工具类 + +“哥,数组专用工具类是专门用来操作数组的吗?比如说创建数组、数组排序、数组检索等等。”三妹的提问其实已经把答案说了出来。 + +“是滴,这里说的数组专用工具类指的是 `java.util.Arrays` 类,基本上常见的数组操作,这个类都提供了静态方法可供直接调用。毕竟数组本身想完成这些操作还是挺麻烦的,有了这层封装,就方便多了。”在回答三妹的同时,我打开 Intellij IDEA,找到了 Arrays 类的源码。 + +```java +package java.util; +/** + * @author Josh Bloch + * @author Neal Gafter + * @author John Rose + * @since 1.2 + */ +public class Arrays {} +``` + +“具体来说,数组操作可分为以下 9 种。” + +- 创建数组 +- 比较数组 +- 数组排序 +- 数组检索 +- 数组转流 +- 打印数组 +- 数组转 List +- setAll(没想好中文名) +- parallelPrefix(没想好中文名) + +“我们来一个一个学习。” + +### 01、创建数组 + +使用 Arrays 类创建数组可以通过以下三个方法: + +- copyOf,复制指定的数组,截取或用 null 填充 +- copyOfRange,复制指定范围内的数组到一个新的数组 +- fill,对数组进行填充 + +1)copyOf,直接来看例子: + +```java +String[] intro = new String[] { "沉", "默", "王", "二" }; +String[] revised = Arrays.copyOf(intro, 3); +String[] expanded = Arrays.copyOf(intro, 5); +System.out.println(Arrays.toString(revised)); +System.out.println(Arrays.toString(expanded)); +``` + +revised 和 expanded 是复制后的新数组,长度分别是 3 和 5,指定的数组长度是 4。来看一下输出结果: + +``` +[沉, 默, 王] +[沉, 默, 王, 二, null] +``` + +看到没?revised 截取了最后一位,因为长度是 3 嘛;expanded 用 null 填充了一位,因为长度是 5。 + +ArrayList(内部的数据结构用的就是数组)源码中的 `grow()` 方法就调用了 `copyOf()` 方法:当 ArrayList 初始大小不满足元素的增长时就会扩容。 + +```java +private Object[] grow(int minCapacity) { + return elementData = Arrays.copyOf(elementData, + newCapacity(minCapacity)); +} +``` + +2)copyOfRange,直接来看例子: + +```java +String[] intro = new String[] { "沉", "默", "王", "二" }; +String[] abridgement = Arrays.copyOfRange(intro, 0, 3); +System.out.println(Arrays.toString(abridgement)); +``` + +`copyOfRange()` 方法需要三个参数,第一个是指定的数组,第二个是起始位置(包含),第三个是截止位置(不包含)。来看一下输出结果: + +```java +[沉, 默, 王] +``` + +0 的位置是“沉”,3 的位置是“二”,也就是说截取了从 0 位(包含)到 3 位(不包含)的数组元素。那假如说下标超出了数组的长度,会发生什么呢? + +```java +String[] abridgementExpanded = Arrays.copyOfRange(intro, 0, 6); +System.out.println(Arrays.toString(abridgementExpanded)); +``` + +结束位置此时为 6,超出了指定数组的长度 4,来看一下输出结果: + +``` +[沉, 默, 王, 二, null, null] +``` + +仍然使用了 null 进行填充。 + +“为什么要这么做呢?”经过这段时间的学习,三妹的眼光越来越毒辣了,问的问题都恰到好处。 + +“嗯,我想是 Arrays 的设计者考虑到了数组越界的问题,不然每次调用 Arrays 类就要先判断很多次长度,很麻烦。”稍作思考后,我给出了这样一个回答。 + + +3)fill,直接来看例子: + +```java +String[] stutter = new String[4]; +Arrays.fill(stutter, "沉默王二"); +System.out.println(Arrays.toString(stutter)); +``` + +使用 new 关键字创建了一个长度为 4 的数组,然后使用 `fill()` 方法将 4 个位置填充为“沉默王二”,来看一下输出结果: + +``` +[沉默王二, 沉默王二, 沉默王二, 沉默王二] +``` + +如果想要一个元素完全相同的数组时, `fill()` 方法就派上用场了。 + +### 02、比较数组 + +Arrays 类的 `equals()` 方法用来判断两个数组是否相等,来看下面这个例子: + +```java +String[] intro = new String[] { "沉", "默", "王", "二" }; +boolean result = Arrays.equals(new String[] { "沉", "默", "王", "二" }, intro); +System.out.println(result); +boolean result1 = Arrays.equals(new String[] { "沉", "默", "王", "三" }, intro); +System.out.println(result1); +``` + +输出结果如下所示: + +``` +true +false +``` + +指定的数组为沉默王二四个字,比较的数组一个是沉默王二,一个是沉默王三,所以 result 为 true,result1 为 false。 + +简单看一下 `equals()` 方法的源码: + +```java +public static boolean equals(Object[] a, Object[] a2) { + if (a==a2) + return true; + if (a==null || a2==null) + return false; + + int length = a.length; + if (a2.length != length) + return false; + + for (int i=0; i fence(1) + at java.base/java.util.Spliterators.checkFromToBounds(Spliterators.java:387) +``` + +### 06、打印数组 + +因为数组是一个对象,直接 `System.out.println` 的话,结果是这样的: + +``` +[Ljava.lang.String;@3d075dc0 +``` + +最优雅的打印方式,是使用 `Arrays.toString()`,来看一下该方法的源码: + +```java +public static String toString(Object[] a) { + if (a == null) + return "null"; + + int iMax = a.length - 1; + if (iMax == -1) + return "[]"; + + StringBuilder b = new StringBuilder(); + b.append('['); + for (int i = 0; ; i++) { + b.append(String.valueOf(a[i])); + if (i == iMax) + return b.append(']').toString(); + b.append(", "); + } +} +``` + +- 先判断 null,是的话,直接返回“null”字符串; +- 获取数组的长度,如果数组的长度为 0( 等价于 length - 1 为 -1),返回中括号“[]”,表示数组为空的; +- 如果数组既不是 null,长度也不为 0,就声明 StringBuilder 对象,然后添加一个数组的开始标记“[”,之后再遍历数组,把每个元素添加进去;其中一个小技巧就是,当遇到末尾元素的时候(i == iMax),不再添加逗号和空格“, ”,而是添加数组的闭合标记“]”。 + +“哥,我能不能问一个问题呀?” + +“你问啊。” + +“就是为什么判断数组长度为 0 的时候判断的是减 1 后比较 -1 呢?为什么不直接比较 0 呢?” + +“呀,你这个问题问的很妙啊!”我想到三妹说一句“respect”,很强!“其实是和遍历数组的时候判断 `i == iMax` 有关了,否则这里就要用 `i == iMax -1` 来判断是否到达数组的最后一个元素了。” + +“哦----------”三妹似乎明白了什么。 + +### 07、数组转 List + +尽管数组非常强大,但它自身可以操作的工具方法很少,比如说判断数组中是否包含某个值。如果能转成 List 的话,就简便多了,因为 Java 的集合框架 List 中封装了很多常用的方法。 + +```java +String[] intro = new String[] { "沉", "默", "王", "二" }; +List rets = Arrays.asList(intro); +System.out.println(rets.contains("二")); +``` + +不过需要注意的是,`Arrays.asList()` 返回的是 `java.util.Arrays.ArrayList`,并不是 `java.util.ArrayList`,它的长度是固定的,无法进行元素的删除或者添加。 + +```java +rets.add("三"); +rets.remove("二"); +``` + +这个在编码的时候一定要注意,否则在执行这两个方法的时候,会抛出异常: + +``` +Exception in thread "main" java.lang.UnsupportedOperationException + at java.base/java.util.AbstractList.add(AbstractList.java:153) + at java.base/java.util.AbstractList.add(AbstractList.java:111) +``` + +要想操作元素的话,需要多一步转化,转成真正的 `java.util.ArrayList`: + +```java +List rets1 = new ArrayList<>(Arrays.asList(intro)); +rets1.add("三"); +rets1.remove("二"); +``` + +### 08、setAll + +Java 8 新增了 `setAll()` 方法,它提供了一个函数式编程的入口,可以对数组的元素进行填充: + +```java +int[] array = new int[10]; +Arrays.setAll(array, i -> i * 10); +System.out.println(Arrays.toString(array)); +``` + +“这段代码什么意思呢?”三妹问。 + +i 就相当于是数组的下标,值从 0 开始,到 9 结束,那么 `i * 10` 就意味着值从 0 * 10 开始,到 9 * 10 结束,来看一下输出结果: + +``` +[0, 10, 20, 30, 40, 50, 60, 70, 80, 90] +``` + +可以用来为新数组填充基于原来数组的新元素。 + +### 09、parallelPrefix + +`parallelPrefix()` 方法和 `setAll()` 方法一样,也是 Java 8 之后提供的,提供了一个函数式编程的入口,通过遍历数组中的元素,将当前下标位置上的元素与它之前下标的元素进行操作,然后将操作后的结果覆盖当前下标位置上的元素。 + +```java +int[] arr = new int[] { 1, 2, 3, 4}; +Arrays.parallelPrefix(arr, (left, right) -> left + right); +System.out.println(Arrays.toString(arr)); +``` + +上面代码中有一个 Lambda 表达式(`(left, right) -> left + right`),是什么意思呢?上面这段代码等同于: + +```java +int[] arr = new int[]{1, 2, 3, 4}; +Arrays.parallelPrefix(arr, (left, right) -> { + System.out.println(left + "," + right); + return left + right; +}); +System.out.println(Arrays.toString(arr)); +``` + +来看一下输出结果就明白了: + +``` +1,2 +3,3 +6,4 +[1, 3, 6, 10] +``` + +也就是说, Lambda 表达式执行了三次: + +- 第一次是 1 和 2 相加,结果是 3,替换下标为 1 的位置 +- 第二次是 3 和 3 相加,结果是 6,也就是第一次的结果和下标为 2 的元素相加的结果 +- 第三次是 6 和 4 相加,结果是 10,也就是第二次的结果和下标为 3 的元素相加的结果 + + +### 10、总结 + +“好了,三妹,就先学到这吧。如果你以后翻 Java 源码的时候,只要是用到数组的,尤其是 ArrayList 类,就可以看到 Arrays 类的很多影子。” + +“嗯嗯,我先复习一下这节的内容。哥,你去休息吧。” + +我来到客厅,坐到沙发上,捧起黄永玉先生的《无愁河上的浪荡汉子·八年卷 1》看了起来,津津有味。。。。。。 \ No newline at end of file diff --git a/docs/array/gailan.md b/docs/array/gailan.md new file mode 100644 index 0000000000000000000000000000000000000000..1dd98c4bcbdcfb4a0097ac6210419953d6ff8030 --- /dev/null +++ b/docs/array/gailan.md @@ -0,0 +1,246 @@ +## 数组概览 + +“哥,我看你之前的文章里提到,ArrayList 的内部是用数组实现的,我就对数组非常感兴趣,想深入地了解一下,今天终于到这个环节了,好期待呀!”三妹的语气里显得很兴奋。 + +“的确是的,看 ArrayList 的源码就一清二楚了。”我一边说,一边打开 Intellij IDEA,并找到了 ArrayList 的源码。 + +```java +/** + * The array buffer into which the elements of the ArrayList are stored. + * The capacity of the ArrayList is the length of this array buffer. Any + * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA + * will be expanded to DEFAULT_CAPACITY when the first element is added. + */ +transient Object[] elementData; // non-private to simplify nested class access + +/** + * The size of the ArrayList (the number of elements it contains). + * + * @serial + */ +private int size; +``` + +“瞧见没?`Object[] elementData` 就是数组。”我指着显示屏上这串代码继续说。 + +数组是一个对象,它包含了一组固定数量的元素,并且这些元素的类型是相同的。数组会按照索引的方式将元素放在指定的位置上,意味着我们可以通过索引来访问这些元素。在 Java 中,索引是从 0 开始的。 + +“哥,能说一下为什么索引从 0 开始吗?”三妹突然这个话题很感兴趣。 + +“哦,Java 是基于 C/C++ 语言实现的,而 C 语言的下标是从 0 开始的,所以 Java 就继承了这个良好的传统习惯。C语言有一个很重要概念,叫做指针,它实际上是一个偏移量,距离开始位置的偏移量,第一个元素就在开始的位置,它的偏移量就为 0,所以索引就为 0。”此刻,我很自信。 + +“此外,还有另外一种说法。早期的计算机资源比较匮乏,0 作为起始下标相比较于 1 作为起始下标,编译的效率更高。” + +“哦。”三妹意味深长地点了点头。 + +我们可以将数组理解为一个个整齐排列的单元格,每个单元格里面存放着一个元素。 + +数组元素的类型可以是基本数据类型(比如说 int、double),也可以是引用数据类型(比如说 String),包括自定义类型。 + +数组的声明方式分两种。 + +先来看第一种: + +```java +int[] anArray; +``` + +再来看第二种: + +```java +int anOtherArray[]; +``` + +不同之处就在于中括号的位置,是跟在类型关键字的后面,还是跟在变量的名称的后面。前一种的使用频率更高一些,像 ArrayList 的源码中就用了第一种方式。 + +同样的,数组的初始化方式也有多种,最常见的是: + +```java +int[] anArray = new int[10]; +``` + +看到了没?上面这行代码中使用了 new 关键字,这就意味着数组的确是一个对象,只有对象的创建才会用到 new 关键字,基本数据类型是不用的。然后,我们需要在方括号中指定数组的长度。 + +这时候,数组中的每个元素都会被初始化为默认值,int 类型的就为 0,Object 类型的就为 null。 不同数据类型的默认值不同,可以参照[之前的文章](https://mp.weixin.qq.com/s/twim3w_dp5ctCigjLGIbFw)。 + +另外,还可以使用大括号的方式,直接初始化数组中的元素: + +```java +int anOtherArray[] = new int[] {1, 2, 3, 4, 5}; +``` + +这时候,数组的元素分别是 1、2、3、4、5,索引依次是 0、1、2、3、4,长度是 5。 + +“哥,怎么访问数组呢?”三妹及时地插话到。 + +前面提到过,可以通过索引来访问数组的元素,就像下面这样: + +```java +anArray[0] = 10; +``` + +变量名,加上中括号,加上元素的索引,就可以访问到数组,通过“=”操作符可以对元素进行赋值。 + +如果索引的值超出了数组的界限,就会抛出 `ArrayIndexOutOfBoundException`。 + +既然数组的索引是从 0 开始,那就是到数组的 `length - 1` 结束,不要使用超出这个范围内的索引访问数组,就不会抛出数组越界的异常了。 + +当数组的元素非常多的时候,逐个访问数组就太辛苦了,所以需要通过遍历的方式。 + +第一种,使用 for 循环: + +```java +int anOtherArray[] = new int[] {1, 2, 3, 4, 5}; +for (int i = 0; i < anOtherArray.length; i++) { + System.out.println(anOtherArray[i]); +} +``` + +通过 length 属性获取到数组的长度,然后从 0 开始遍历,就得到了数组的所有元素。 + +第二种,使用 for-each 循环: + +```java +for (int element : anOtherArray) { + System.out.println(element); +} +``` + +如果不需要关心索引的话(意味着不需要修改数组的某个元素),使用 for-each 遍历更简洁一些。当然,也可以使用 while 和 do-while 循环。 + +在 Java 中,可变参数用于将任意数量的参数传递给方法,来看 `varargsMethod()` 方法: + +```java +void varargsMethod(String... varargs) {} +``` + +该方法可以接收任意数量的字符串参数,可以是 0 个或者 N 个,本质上,可变参数就是通过数组实现的。为了证明这一点,我们可以看一下反编译一后的字节码: + +```java +public class VarargsDemo +{ + + public VarargsDemo() + { + } + + transient void varargsMethod(String as[]) + { + } +} +``` + +所以,我们其实可以直接将数组作为参数传递给该方法: + +```java +VarargsDemo demo = new VarargsDemo(); +String[] anArray = new String[] {"沉默王二", "一枚有趣的程序员"}; +demo.varargsMethod(anArray); +``` + +也可以直接传递多个字符串,通过逗号隔开的方式: + +```java +demo.varargsMethod("沉默王二", "一枚有趣的程序员"); +``` + +在 Java 中,数组与 List 关系非常密切。List 封装了很多常用的方法,方便我们对集合进行一些操作,而如果直接操作数组的话,有很多不便,因为数组本身没有提供这些封装好的操作,所以有时候我们需要把数组转成 List。 + +“怎么转呢?”三妹问到。 + +最原始的方式,就是通过遍历数组的方式,一个个将数组添加到 List 中。 + +```java +int[] anArray = new int[] {1, 2, 3, 4, 5}; + +List aList = new ArrayList<>(); +for (int element : anArray) { + aList.add(element); +} +``` + +更优雅的方式是通过 Arrays 类的 `asList()` 方法: + +```java +List aList = Arrays.asList(anArray); +``` + +但需要注意的是,该方法返回的 ArrayList 并不是 `java.util.ArrayList`,它其实是 Arrays 类的一个内部类: + +```java +private static class ArrayList extends AbstractList + implements RandomAccess, java.io.Serializable{} +``` + +如果需要添加元素或者删除元素的话,需要把它转成 `java.util.ArrayList`。 + +```java +new ArrayList<>(Arrays.asList(anArray)); +``` + +Java 8 新增了 Stream 流的概念,这就意味着我们也可以将数组转成 Stream 进行操作。 + +```java +String[] anArray = new String[] {"沉默王二", "一枚有趣的程序员", "好好珍重他"}; +Stream aStream = Arrays.stream(anArray); +``` + + +如果想对数组进行排序的话,可以使用 Arrays 类提供的 `sort()` 方法。 + +- 基本数据类型按照升序排列 +- 实现了 Comparable 接口的对象按照 `compareTo()` 的排序 + +来看第一个例子: + +```java +int[] anArray = new int[] {5, 2, 1, 4, 8}; +Arrays.sort(anArray); +``` + +排序后的结果如下所示: + +```java +[1, 2, 4, 5, 8] +``` + +来看第二个例子: + +```java +String[] yetAnotherArray = new String[] {"A", "E", "Z", "B", "C"}; +Arrays.sort(yetAnotherArray, 1, 3, + Comparator.comparing(String::toString).reversed()); +``` + +只对 1-3 位置上的元素进行反序,所以结果如下所示: + +``` +[A, Z, E, B, C] +``` + +有时候,我们需要从数组中查找某个具体的元素,最直接的方式就是通过遍历的方式: + +```java +int[] anArray = new int[] {5, 2, 1, 4, 8}; +for (int i = 0; i < anArray.length; i++) { + if (anArray[i] == 4) { + System.out.println("找到了 " + i); + break; + } +} +``` + +上例中从数组中查询元素 4,找到后通过 break 关键字退出循环。 + +如果数组提前进行了排序,就可以使用二分查找法,这样效率就会更高一些。`Arrays.binarySearch()` 方法可供我们使用,它需要传递一个数组,和要查找的元素。 + +```java +int[] anArray = new int[] {1, 2, 3, 4, 5}; +int index = Arrays.binarySearch(anArray, 4); +``` + +“除了一维数组,还有二维数组,三妹你可以去研究下,比如说用二维数组打印一下杨辉三角。”说完,我就去阳台上休息了,留三妹在那里学习,不能打扰她。 + +------- + +**点赞越多,更新的动力越足哟,疯狂暗示**~ \ No newline at end of file diff --git a/docs/array/print.md b/docs/array/print.md new file mode 100644 index 0000000000000000000000000000000000000000..1bf2a0f7ff8d2bb525f9a8eaa63c7c23559c0bac --- /dev/null +++ b/docs/array/print.md @@ -0,0 +1,152 @@ +## 打印数组 + +“哥,之前听你说,数组也是一个对象,但 Java 中并未明确的定义这样一个类。”看来三妹有在用心地学习。 + +“是的,因此数组也就没有机会覆盖 `Object.toString()` 方法。如果尝试直接打印数组的话,输出的结果并不是我们预期的结果。”我接着三妹的话继续说。 + +“那怎么打印数组呢?”三妹心有灵犀地把今天的核心问题提了出来。 + +“首先,我们来看一下,为什么不能直接打印数组,直接打印的话,会出现什么问题。” + +来看这样一个例子。 + +``` +String [] cmowers = {"沉默","王二","一枚有趣的程序员"}; +System.out.println(cmowers); +``` + +程序打印的结果是: + +``` +[Ljava.lang.String;@3d075dc0 +``` + +`[Ljava.lang.String;` 表示字符串数组的 Class 名,@ 后面的是十六进制的 hashCode——这样的打印结果太“人性化”了,一般人表示看不懂!为什么会这样显示呢?查看一下 `java.lang.Object` 类的 `toString()` 方法就明白了。 + +```java +public String toString() { + return getClass().getName() + "@" + Integer.toHexString(hashCode()); +} +``` + +再次证明,数组虽然没有显式定义成一个类,但它的确是一个对象,继承了祖先类 Object 的所有方法。 + +“哥,那为什么数组不单独定义一个类来表示呢?就像字符串 String 类那样呢?”三妹这个问题让人头大,但也好解释。 + +“一个合理的说法是 Java 将其隐藏了。假如真的存在这么一个类,就叫 Array.java 吧,我们假想一下它真实的样子,必须得有一个容器来存放数组的每一个元素,就像 String 类那样。”一边回答三妹,我一边打开了 String 类的源码。 + +```java +public final class String + implements java.io.Serializable, Comparable, CharSequence { + /** The value is used for character storage. */ + private final char value[]; +} +``` + +“最终还是要用类似一种数组的形式来存放数组的元素,对吧?这就变得很没有必要了,不妨就把数组当做是一个没有形体的对象吧!” + +“好了,不讨论这个了。”我怕话题扯远了,扯到我自己也答不出来就尴尬了,赶紧把三妹的思路拽了回来。 + +“我们来看第一种打印数组的方法,使用时髦一点的 Stream 流。” + +第一种形式: + +```java +Arrays.asList(cmowers).stream().forEach(s -> System.out.println(s)); +``` + +第二种形式: + +```java +Stream.of(cmowers).forEach(System.out::println); +``` + +第三种形式: + +```java +Arrays.stream(cmowers).forEach(System.out::println); +``` + +打印的结果如下所示。 + +``` +沉默 +王二 +一枚有趣的程序员 +``` + +没错,这三种方式都可以轻松胜任本职工作,并且显得有点高大上,毕竟用到了 Stream,以及 lambda 表达式。 + +“当然了,也可以使用比较土的方式,for 循环。甚至 for-each 也行。” + +```java +for(int i = 0; i < cmowers.length; i++){ + System.out.println(cmowers[i]); +} + +for (String s : cmowers) { + System.out.println(s); +} +``` + +“哥,你难道忘了[上一篇](https://mp.weixin.qq.com/s/acnDNH6A8USm_EYIT6i-jA)在讲 Arrays 工具类的时候,提到过另外一种方法 `Arrays.toString()` 吗?”三妹看我一直说不到点子上,有点着急了。 + +“当然没有了,我认为 `Arrays.toString()` 是打印数组的最佳方式,没有之一。”我的情绪有点激动。 + +`Arrays.toString()` 可以将任意类型的数组转成字符串,包括基本类型数组和引用类型数组。该方法有多种重载形式。 + +![](https://cdn.jsdelivr.net/gh/itwanger/Tech-Sister-Learn-Java/images/array/print-01.png) + +使用 `Arrays.toString()` 方法来打印数组再优雅不过了,就像,就像,就像蒙娜丽莎的微笑。 + +![](https://cdn.jsdelivr.net/gh/itwanger/Tech-Sister-Learn-Java/images/array/print-02.png) + +(三妹看到这么一副图的时候忍不住地笑了) + +“三妹,你不要笑,来,怀揣着愉快的心情看一下代码示例。” + +```java +String [] cmowers = {"沉默","王二","一枚有趣的程序员"}; +System.out.println(Arrays.toString(cmowers)); +``` + +程序打印结果: + +``` +[沉默, 王二, 一枚有趣的程序员] +``` + +哇,打印格式不要太完美,不多不少!完全是我们预期的结果:`[]` 表明是一个数组,`,` 点和空格用来分割元素。 + +“哥,那如果我想打印二维数组呢?” + +“可以使用 `Arrays.deepToString()` 方法。” + +```java +String[][] deepArray = new String[][] {{"沉默", "王二"}, {"一枚有趣的程序员"}}; +System.out.println(Arrays.deepToString(deepArray)); +``` + +打印结果如下所示。 + +``` +[[沉默, 王二], [一枚有趣的程序员]] +``` + +------- + +“说到打印,三妹,哥给你提醒一点。阿里巴巴的 Java 开发手册上有这样一条规约,你看。” + +![](https://cdn.jsdelivr.net/gh/itwanger/Tech-Sister-Learn-Java/images/array/print-03.png) + +“什么是 POJO 呢,就是 Plain Ordinary Java Object 的缩写,一般在 Web 应用程序中建立一个数据库的映射对象时,我们称它为 POJO,这类对象不继承或不实现任何其它 Java 框架的类或接口。” + +“对于这样的类,最好是重写一下它的 `toString()` 方法,方便查看这个对象到底包含了什么字段,好排查问题。” + +“如果不重写的话,打印出来的 Java 对象就像直接打印数组的那样,一串谁也看不懂的字符序列。” + +“可以借助 Intellij IDEA 生成重写的 `toString()` 方法,特别方便。” + +“好的,哥,我记住了。以后遇到的话,我注意下。你去休息吧,我来敲一下你提到的这些代码,练一练。” + +“OK,我走,我走。” diff --git a/docs/object-class/box.md b/docs/object-class/box.md new file mode 100644 index 0000000000000000000000000000000000000000..246c65ede8062e9f470384ee020a21d733ed0702 --- /dev/null +++ b/docs/object-class/box.md @@ -0,0 +1,256 @@ +## 自动装箱与拆箱 + +“哥,听说 Java 的每个基本类型都对应了一个包装类型,比如说 int 的包装类型为 Integer,double 的包装类型为 Double,是这样吗?”从三妹这句话当中,能听得出来,她已经提前预习这块内容了。 + +“是的,三妹。基本类型和包装类型的区别主要有以下 4 点,我来带你学习一下。”我回答说。我们家的斜对面刚好是一所小学,所以时不时还能听到朗朗的读书声,让人心情非常愉快。 + +“三妹,你准备好了吗?我们开始吧。” + +“第一,**包装类型可以为 null,而基本类型不可以**。别小看这一点区别,它使得包装类型可以应用于 POJO 中,而基本类型则不行。” + +“POJO 是什么呢?”遇到不会的就问,三妹在这一点上还是非常兢兢业业的。 + +“POJO 的英文全称是 Plain Ordinary Java Object,翻译一下就是,简单无规则的 Java 对象,只有字段以及对应的 setter 和 getter 方法。” + +```java +class Writer { + private Integer age; + private String name; + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} +``` + +和 POJO 类似的,还有数据传输对象 DTO(Data Transfer Object,泛指用于展示层与服务层之间的数据传输对象)、视图对象 VO(View Object,把某个页面的数据封装起来)、持久化对象 PO(Persistant Object,可以看成是与数据库中的表映射的 Java 对象)。 + +“那为什么 POJO 的字段必须要用包装类型呢?”三妹问。 + +“《阿里巴巴 Java 开发手册》上有详细的说明,你看。”我打开 PDF,并翻到了对应的内容,指着屏幕念道。 + +>数据库的查询结果可能是 null,如果使用基本类型的话,因为要自动拆箱,就会抛出 NullPointerException 的异常。 + +“什么是自动拆箱呢?” + +“自动拆箱指的是,将包装类型转为基本类型,比如说把 Integer 对象转换成 int 值;对应的,把基本类型转为包装类型,则称为自动装箱。” + +“哦。” + +“那接下来,我们来看第二点不同。**包装类型可用于泛型,而基本类型不可以**,否则就会出现编译错误。”一边说着,我一边在 Intellij IDEA 中噼里啪啦地敲了起来。 + +“三妹,你瞧,编译器提示错误了。” + +```java +List list = new ArrayList<>(); // 提示 Syntax error, insert "Dimensions" to complete ReferenceType +List list = new ArrayList<>(); +``` + +“为什么呢?”三妹及时地问道。 + +“因为泛型在编译时会进行类型擦除,最后只保留原始类型,而原始类型只能是 Object 类及其子类——基本类型是个例外。” + +“那,接下来,我们来说第三点,**基本类型比包装类型更高效**。”我喝了一口茶继续说道。 + +“作为局部变量时,基本类型在栈中直接存储的具体数值,而包装类型则存储的是堆中的引用。”我一边说着,一边打开 `draw.io` 画起了图。 + +![](https://cdn.jsdelivr.net/gh/itwanger/Tech-Sister-Learn-Java/images/object-class/box-01.png) + +很显然,相比较于基本类型而言,包装类型需要占用更多的内存空间,不仅要存储对象,还要存储引用。假如没有基本类型的话,对于数值这类经常使用到的数据来说,每次都要通过 new 一个包装类型就显得非常笨重。 + +“三妹,你想知道程序运行时,数据都存储在什么地方吗?” + +“嗯嗯,哥,你说说呗。” + +“通常来说,有 4 个地方可以用来存储数据。” + +1)寄存器。这是最快的存储区,因为它位于 CPU 内部,用来暂时存放参与运算的数据和运算结果。 + +2)栈。位于 RAM(Random Access Memory,也叫主存,与 CPU 直接交换数据的内部存储器)中,速度仅次于寄存器。但是,在分配内存的时候,存放在栈中的数据大小与生存周期必须在编译时是确定的,缺乏灵活性。基本数据类型的值和对象的引用通常存储在这块区域。 + +3)堆。也位于 RAM 区,可以动态分配内存大小,编译器不必知道要从堆里分配多少存储空间,生存周期也不必事先告诉编译器,Java 的垃圾收集器会自动收走不再使用的数据,因此可以得到更大的灵活性。但是,运行时动态分配内存和销毁对象都需要占用时间,所以效率比栈低一些。new 创建的对象都会存储在这块区域。 + +4)磁盘。如果数据完全存储在程序之外,就可以不受程序的限制,在程序没有运行时也可以存在。像文件、数据库,就是通过持久化的方式,让对象存放在磁盘上。当需要的时候,再反序列化成程序可以识别的对象。 + +“能明白吗?三妹?” + +“这节讲完后,我再好好消化一下。” + +“那好,我们来说第四点,**两个包装类型的值可以相同,但却不相等**。” + +```java +Integer chenmo = new Integer(10); +Integer wanger = new Integer(10); + +System.out.println(chenmo == wanger); // false +System.out.println(chenmo.equals(wanger )); // true +``` + +“两个包装类型在使用“==”进行判断的时候,判断的是其指向的地址是否相等,由于是两个对象,所以地址是不同的。” + +“而 chenmo.equals(wanger) 的输出结果为 true,是因为 equals() 方法内部比较的是两个 int 值是否相等。” + +```java +private final int value; + +public int intValue() { + return value; +} +public boolean equals(Object obj) { + if (obj instanceof Integer) { + return value == ((Integer)obj).intValue(); + } + return false; +} +``` + +虽然 chenmo 和 wanger 的值都是 10,但他们并不相等。换句话说就是:将“==”操作符应用于包装类型比较的时候,其结果很可能会和预期的不符。 + +“三妹,瞧,`((Integer)obj).intValue()` 这段代码就是用来自动拆箱的。下面,我们来详细地说一说自动装箱和自动拆箱。” + +既然有基本类型和包装类型,肯定有些时候要在它们之间进行转换。把基本类型转换成包装类型的过程叫做装箱(boxing)。反之,把包装类型转换成基本类型的过程叫做拆箱(unboxing)。 + +在 Java 1.5 之前,开发人员要手动进行装拆箱,比如说: + +```java +Integer chenmo = new Integer(10); // 手动装箱 +int wanger = chenmo.intValue(); // 手动拆箱 +``` + +Java 1.5 为了减少开发人员的工作,提供了自动装箱与自动拆箱的功能。这下就方便了。 + +```jav +Integer chenmo = 10; // 自动装箱 +int wanger = chenmo; // 自动拆箱 +``` + +来看一下反编译后的代码。 + +```java +Integer chenmo = Integer.valueOf(10); +int wanger = chenmo.intValue(); +``` + +也就是说,自动装箱是通过 `Integer.valueOf()` 完成的;自动拆箱是通过 `Integer.intValue()` 完成的。 + +“嗯,三妹,给你出一道面试题吧。” + +```java +// 1)基本类型和包装类型 +int a = 100; +Integer b = 100; +System.out.println(a == b); + +// 2)两个包装类型 +Integer c = 100; +Integer d = 100; +System.out.println(c == d); + +// 3) +c = 200; +d = 200; +System.out.println(c == d); +``` + +“给你 3 分钟时间,你先思考下,我去抽根华子,等我回来,然后再来分析一下为什么。” + +。。。。。。 + +“嗯,哥,你过来吧,我说一说我的想法。” + +第一段代码,基本类型和包装类型进行 == 比较,这时候 b 会自动拆箱,直接和 a 比较值,所以结果为 true。 + +第二段代码,两个包装类型都被赋值为了 100,这时候会进行自动装箱,按照你之前说的,将“==”操作符应用于包装类型比较的时候,其结果很可能会和预期的不符,我想结果可能为 false。 + +第三段代码,两个包装类型重新被赋值为了 200,这时候仍然会进行自动装箱,我想结果仍然为 false。 + +“嗯嗯,三妹,你分析的很有逻辑,但第二段代码的结果为 true,是不是感到很奇怪?” + +“为什么会这样呀?”三妹急切地问。 + +“你说的没错,自动装箱是通过 Integer.valueOf() 完成的,我们来看看这个方法的源码就明白为什么了。” + +```java +public static Integer valueOf(int i) { + if (i >= IntegerCache.low && i <= IntegerCache.high) + return IntegerCache.cache[i + (-IntegerCache.low)]; + return new Integer(i); +} +``` + +是不是看到了一个之前从来没见过的类——IntegerCache? + +“难道说是 Integer 的缓存类?”三妹做出了自己的判断。 + +“是的,来看一下 IntegerCache 的源码吧。” + +```java +private static class IntegerCache { + static final int low = -128; + static final int high; + static final Integer cache[]; + + static { + // high value may be configured by property + int h = 127; + int i = parseInt(integerCacheHighPropValue); + i = Math.max(i, 127); + h = Math.min(i, Integer.MAX_VALUE - (-low) -1); + high = h; + + cache = new Integer[(high - low) + 1]; + int j = low; + for(int k = 0; k < cache.length; k++) + cache[k] = new Integer(j++); + + // range [-128, 127] must be interned (JLS7 5.1.7) + assert IntegerCache.high >= 127; + } +} +``` + +大致瞟一下这段代码你就全明白了。-128 到 127 之间的数会从 IntegerCache 中取,然后比较,所以第二段代码(100 在这个范围之内)的结果是 true,而第三段代码(200 不在这个范围之内,所以 new 出来了两个 Integer 对象)的结果是 false。 + +“三妹,看完上面的分析之后,我希望你记住一点:**当需要进行自动装箱时,如果数字在 -128 至 127 之间时,会直接使用缓存中的对象,而不是重新创建一个对象**。” + +“自动装拆箱是一个很好的功能,大大节省了我们开发人员的精力,但也会引发一些麻烦,比如下面这段代码,性能就很差。” + +```java +long t1 = System.currentTimeMillis(); +Long sum = 0L; +for (int i = 0; i < Integer.MAX_VALUE;i++) { + sum += i; +} +long t2 = System.currentTimeMillis(); +System.out.println(t2-t1); +``` + +“知道为什么吗?三妹。” + +“难道是因为 sum 被声明成了包装类型 Long 而不是基本类型 long。”三妹若有所思。 + +“是滴,由于 sum 是个 Long 型,而 i 为 int 类型,`sum += i` 在执行的时候,会先把 i 强转为 long 型,然后再把 sum 拆箱为 long 型进行相加操作,之后再自动装箱为 Long 型赋值给 sum。” + +“三妹,你可以试一下,把 sum 换成 long 型比较一下它们运行的时间。” + +。。。。。。 + +“哇,sum 为 Long 型的时候,足足运行了 5825 毫秒;sum 为 long 型的时候,只需要 679 毫秒。” + +“好了,三妹,今天的主题就先讲到这吧。我再去来根华子。” + +------- + +PS:点击「**阅读原文**」可直达《教妹学Java》专栏的 GitHub 开源地址,记得 star 哦! \ No newline at end of file diff --git a/docs/object-class/deep-copy.md b/docs/object-class/deep-copy.md new file mode 100644 index 0000000000000000000000000000000000000000..54e7abb666c61cfde15e49b918677575fbddb35a --- /dev/null +++ b/docs/object-class/deep-copy.md @@ -0,0 +1,419 @@ +## 浅拷贝与深拷贝 + +“哥,听说浅拷贝和深拷贝是 Java 面试中经常会被问到的一个问题,是这样吗?” + +“还真的是,而且了解浅拷贝和深拷贝的原理,对 Java 是值传递还是引用传递也会有更深的理解。”我肯定地回答。 + +“不管是浅拷贝还是深拷贝,都可以通过调用 Object 类的 `clone()` 方法来完成。”我一边说,一边打开 Intellij IDEA,并找到了 `clone()` 方法的源码。 + +```java +@HotSpotIntrinsicCandidate +protected native Object clone() throws CloneNotSupportedException; +``` + +其中 `@HotSpotIntrinsicCandidate` 是 Java 9 引入的一个注解,被它标注的方法,在 HotSpot 虚拟机中会有一套高效的实现。需要注意的是,`clone()` 方法同时是一个本地(`native`)方法,它的具体实现会交给 HotSpot 虚拟机,那就意味着虚拟机在运行该方法的时候,会将其替换为更高效的 C/C++ 代码,进而调用操作系统去完成对象的克隆工作。 + +“哥,那你就先说浅拷贝吧!” + +“好的呀。直接上实战代码。” + +```java +class Writer implements Cloneable{ + private int age; + private String name; + + // getter/setter 和构造方法都已省略 + + @Override + public String toString() { + return super.toString().substring(26) + "{" + + "age=" + age + + ", name='" + name + '\'' + + '}'; + } + + @Override + protected Object clone() throws CloneNotSupportedException { + return super.clone(); + } +} +``` + +Writer 类有两个字段,分别是 int 类型的 age,和 String 类型的 name。然后重写了 `toString()` 方法,方便打印对象的具体信息。并且重写了 `clone()` 方法,方法体里面也很简单,直接调用 Object 类的 `clone()` 方法。 + +“既然 Writer 类的 `clone()` 方法体里只有一行代码,调用的还是超类 Object 的 `clone()` 方法?为什么还要重写呢?不是多此一举吗?”三妹着急地问。 + +“嗯,是这样的,三妹。Object 类中的 `clone()` 方法是 protected 的,如果 Writer 类不去重写的话,Writer 类的对象是无法调用 `clone()` 方法的,因为 protected 修饰的方法对子类并不可见。” + +“哦哦,那为什么要实现 Cloneable 接口呢?”三妹开启了十万个为什么的模式。 + +Cloneable 接口是一个标记接口,它肚子里面是空的: + +```java +public interface Cloneable { +} +``` + +只是,如果一个类没有实现 Cloneable 接口,即便它重写了 `clone()` 方法,依然是无法调用该方法进行对象克隆的,程序在执行 `clone()` 方法的时候会抛出 CloneNotSupportedException 异常。 + +```java +Exception in thread "main" java.lang.CloneNotSupportedException +``` + +标记接口的作用其实很简单,用来表示某个功能在执行的时候是合法的。 + +“哦,我悟了!”三妹看来是彻底明白了我说的内容。 + +“接着,来测试类。” + +```java +class TestClone { + public static void main(String[] args) throws CloneNotSupportedException { + Writer writer1 = new Writer(18,"二哥"); + Writer writer2 = (Writer) writer1.clone(); + + System.out.println("浅拷贝后:"); + System.out.println("writer1:" + writer1); + System.out.println("writer2:" + writer2); + + writer2.setName("三妹"); + + System.out.println("调整了 writer2 的 name 后:"); + System.out.println("writer1:" + writer1); + System.out.println("writer2:" + writer2); + } +} +``` + +- 通过 new 关键字声明了一个 Writer 对象(18 岁的二哥),将其赋值给 writer1。 +- 通过调用 `clone()` 方法进行对象拷贝,并将其赋值给 writer2。 +- 之后打印 writer1 和 writer2。 +- 将 writer2 的 name 字段调整为“三妹”。 +- 再次打印。 + +来看一下输出结果。 + +``` +浅拷贝后: +writer1:Writer@68837a77{age=18, name='二哥'} +writer2:Writer@b97c004{age=18, name='二哥'} +调整了 writer2 的 name 后: +writer1:Writer@68837a77{age=18, name='二哥'} +writer2:Writer@b97c004{age=18, name='三妹'} +``` + +可以看得出,浅拷贝后,writer1 和 writer2 引用了不同的对象,但值是相同的,说明拷贝成功。之后,修改了 writer2 的 name 字段,直接上图就明白了。 + +![](https://cdn.jsdelivr.net/gh/itwanger/Tech-Sister-Learn-Java/images/object-class/deep-copy-01.png) + + 之前的例子中,Writer 类只有两个字段,没有引用类型字段。那么,我们再来看另外一个例子,为 Writer 类增加一个自定义的引用类型字段 Book,先来看 Book 的定义。 + +```java +class Book { + private String bookName; + private int price; + + // getter/setter 和构造方法都已省略 + + @Override + public String toString() { + return super.toString().substring(26) + + " bookName='" + bookName + '\'' + + ", price=" + price + + '}'; + } +} +``` + +有两个字段,分别是 String 类型的 bookName 和 int 类型的 price。 + +然后来看 Writer 类的定义。 + +```java +class Writer implements Cloneable{ + private int age; + private String name; + private Book book; + + // getter/setter 和构造方法都已省略 + + @Override + public String toString() { + return super.toString().substring(26) + + " age=" + age + + ", name='" + name + '\'' + + ", book=" + book + + '}'; + } + + @Override + protected Object clone() throws CloneNotSupportedException { + return super.clone(); + } +} +``` + +比之前的例子多了一个自定义类型的字段 book,`clone()` 方法并没有任何改变。 + +再来看测试类。 + +```java +class TestClone { + public static void main(String[] args) throws CloneNotSupportedException { + Writer writer1 = new Writer(18,"二哥"); + Book book1 = new Book("编译原理",100); + writer1.setBook(book1); + + Writer writer2 = (Writer) writer1.clone(); + System.out.println("浅拷贝后:"); + + System.out.println("writer1:" + writer1); + System.out.println("writer2:" + writer2); + + Book book2 = writer2.getBook(); + book2.setBookName("永恒的图灵"); + book2.setPrice(70); + System.out.println("writer2.book 变更后:"); + + System.out.println("writer1:" + writer1); + System.out.println("writer2:" + writer2); + } +} +``` + +- 通过 new 关键字声明了一个 Writer 对象(18 岁的二哥),将其赋值给 writer1。 +- 通过 new 关键字声明了一个 Book 对象(100 块的编译原理),将其赋值给 book1。 +- 将 writer1 的 book 字段设置为 book1。 +- 通过调用 `clone()` 方法进行对象拷贝,并将其赋值给 writer2。 +- 之后打印 writer1 和 writer2。 +- 获取 writer2 的 book 字段,并将其赋值给 book2。 +- 将 book2 的 bookName 字段调整为“永恒的图灵”,price 字段调整为 70。 +- 再次打印。 + +来看一下输出结果。 + +``` +浅拷贝后: +writer1:Writer@68837a77 age=18, name='二哥', book=Book@32e6e9c3 bookName='编译原理', price=100}} +writer2:Writer@6d00a15d age=18, name='二哥', book=Book@32e6e9c3 bookName='编译原理', price=100}} +writer2.book 变更后: +writer1:Writer@68837a77 age=18, name='二哥', book=Book@32e6e9c3 bookName='永恒的图灵', price=70}} +writer2:Writer@36d4b5c age=18, name='二哥', book=Book@32e6e9c3 bookName='永恒的图灵', price=70}} +``` + +与之前例子不同的是,writer2.book 变更后,writer1.book 也发生了改变。这是因为字符串 String 是不可变对象,一个新的值必须在字符串常量池中开辟一段新的内存空间,而自定义对象的内存地址并没有发生改变,只是对应的字段值发生了改变,见下图。 + +![](https://cdn.jsdelivr.net/gh/itwanger/Tech-Sister-Learn-Java/images/object-class/deep-copy-02.png) + +“哇,哥,果真一图胜千言,我明白了。”三妹似乎对我画的图很感兴趣呢,“那你继续说深拷贝吧!” + +“嗯,三妹,你有没有注意到,浅拷贝克隆的对象中,引用类型的字段指向的是同一个,当改变任何一个对象,另外一个对象也会随之改变,除去字符串的特殊性外。” + +“深拷贝和浅拷贝不同的,深拷贝中的引用类型字段也会克隆一份,当改变任何一个对象,另外一个对象不会随之改变。” + +“明白了这一点后,我们再来看例子。” + +```java +class Book implements Cloneable{ + private String bookName; + private int price; + + // getter/setter 和构造方法都已省略 + + @Override + public String toString() { + return super.toString().substring(26) + + " bookName='" + bookName + '\'' + + ", price=" + price + + '}'; + } + + @Override + protected Object clone() throws CloneNotSupportedException { + return super.clone(); + } +} +``` + +注意,此时的 Book 类和浅拷贝时不同,重写了 `clone()` 方法,并实现了 Cloneable 接口。为的就是深拷贝的时候也能够克隆该字段。 + +```java +class Writer implements Cloneable{ + private int age; + private String name; + private Book book; + + // getter/setter 和构造方法都已省略 + + @Override + public String toString() { + return super.toString().substring(26) + + " age=" + age + + ", name='" + name + '\'' + + ", book=" + book + + '}'; + } + + @Override + protected Object clone() throws CloneNotSupportedException { + Writer writer = (Writer) super.clone(); + writer.setBook((Book) writer.getBook().clone()); + return writer; + } +} +``` + +注意,此时 Writer 类也与之前的不同,`clone()` 方法当中,不再只调用 Object 的 `clone()` 方法对 Writer 进行克隆了,还对 Book 也进行了克隆。 + +来看测试类。 + +```java +class TestClone { + public static void main(String[] args) throws CloneNotSupportedException { + Writer writer1 = new Writer(18,"二哥"); + Book book1 = new Book("编译原理",100); + writer1.setBook(book1); + + Writer writer2 = (Writer) writer1.clone(); + System.out.println("深拷贝后:"); + + System.out.println("writer1:" + writer1); + System.out.println("writer2:" + writer2); + + Book book2 = writer2.getBook(); + book2.setBookName("永恒的图灵"); + book2.setPrice(70); + System.out.println("writer2.book 变更后:"); + + System.out.println("writer1:" + writer1); + System.out.println("writer2:" + writer2); + } +} +``` + +这个测试类和之前的浅拷贝的测试类就完全一样了,但运行结果是不同的。 + +``` +深拷贝后: +writer1:Writer@6be46e8f age=18, name='二哥', book=Book@5056dfcb bookName='编译原理', price=100}} +writer2:Writer@6d00a15d age=18, name='二哥', book=Book@51efea79 bookName='编译原理', price=100}} +writer2.book 变更后: +writer1:Writer@6be46e8f age=18, name='二哥', book=Book@5056dfcb bookName='编译原理', price=100}} +writer2:Writer@6d00a15d age=18, name='二哥', book=Book@51efea79 bookName='永恒的图灵', price=70}} +``` + +不只是 writer1 和 writer2 是不同的对象,它们中的 book 也是不同的对象。所以,改变了 writer2 中的 book 并不会影响到 writer1。 + +![](https://cdn.jsdelivr.net/gh/itwanger/Tech-Sister-Learn-Java/images/object-class/deep-copy-03.png) + +不过,通过 `clone()` 方法实现的深拷贝比较笨重,因为要将所有的引用类型都重写 `clone()` 方法,当嵌套的对象比较多的时候,就废了! + +“那有没有好的办法呢?”三妹急切的问。 + +“当然有了,利用序列化。”我胸有成竹的回答,“序列化是将对象写到流中便于传输,而反序列化则是将对象从流中读取出来。” + +“写入流中的对象就是对原始对象的拷贝。需要注意的是,每个要序列化的类都要实现 Serializable 接口,该接口和 Cloneable 接口类似,都是标记型接口。” + +来看例子。 + +```java +class Book implements Serializable { + private String bookName; + private int price; + + // getter/setter 和构造方法都已省略 + + @Override + public String toString() { + return super.toString().substring(26) + + " bookName='" + bookName + '\'' + + ", price=" + price + + '}'; + } +} +``` + +Book 需要实现 Serializable 接口。 + +```java +class Writer implements Serializable { + private int age; + private String name; + private Book book; + + // getter/setter 和构造方法都已省略 + + @Override + public String toString() { + return super.toString().substring(26) + + " age=" + age + + ", name='" + name + '\'' + + ", book=" + book + + '}'; + } + + //深度拷贝 + public Object deepClone() throws IOException, ClassNotFoundException { + // 序列化 + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(bos); + + oos.writeObject(this); + + // 反序列化 + ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(bis); + + return ois.readObject(); + } +} +``` + +Writer 类也需要实现 Serializable 接口,并且在该类中,增加了一个 `deepClone()` 的方法,利用 OutputStream 进行序列化,InputStream 进行反序列化,这样就实现了深拷贝。 + +来看示例。 + +```java +class TestClone { + public static void main(String[] args) throws IOException, ClassNotFoundException { + Writer writer1 = new Writer(18,"二哥"); + Book book1 = new Book("编译原理",100); + writer1.setBook(book1); + + Writer writer2 = (Writer) writer1.deepClone(); + System.out.println("深拷贝后:"); + + System.out.println("writer1:" + writer1); + System.out.println("writer2:" + writer2); + + Book book2 = writer2.getBook(); + book2.setBookName("永恒的图灵"); + book2.setPrice(70); + System.out.println("writer2.book 变更后:"); + + System.out.println("writer1:" + writer1); + System.out.println("writer2:" + writer2); + } +} +``` + +与之前测试类不同的是,调用了 `deepClone()` 方法。 + +``` +深拷贝后: +writer1:Writer@9629756 age=18, name='二哥', book=Book@735b5592 bookName='编译原理', price=100}} +writer2:Writer@544fe44c age=18, name='二哥', book=Book@31610302 bookName='编译原理', price=100}} +writer2.book 变更后: +writer1:Writer@9629756 age=18, name='二哥', book=Book@735b5592 bookName='编译原理', price=100}} +writer2:Writer@544fe44c age=18, name='二哥', book=Book@31610302 bookName='永恒的图灵', price=70}} +``` + +测试结果和之前用 `clone()` 方法实现的深拷贝类似。 + +“不过,三妹,需要注意,由于是序列化涉及到输入流和输出流的读写,在性能上要比 HotSpot 虚拟机实现的 `clone()` 方法差很多。”我语重心长地说。 + +“好的,二哥,你先去休息吧,让我来琢磨一会,总结一下浅拷贝和深拷贝之间的差异。” + +“嗯嗯。” + +PS:点击「**阅读原文**」可直达《教妹学Java》专栏的 GitHub 开源地址,记得 star 哦! \ No newline at end of file diff --git a/docs/object-class/java-abstract.md b/docs/object-class/java-abstract.md new file mode 100644 index 0000000000000000000000000000000000000000..6415e6fa9dbbb8f49006dcdac7ad0b6d9841ae92 --- /dev/null +++ b/docs/object-class/java-abstract.md @@ -0,0 +1,241 @@ +## 抽象类 + +“二哥,你这明显加快了更新的频率呀!”三妹对于我最近的肝劲由衷的佩服了起来。 + +“哈哈,是呀,这次不能再断更了,我要再更 175 篇,总计 200 篇,给广大的学弟学妹们一个完整的 Java 学习体系。”我对未来充满了信心。 + +“那就开始吧。”三妹说。 + +------- + + +定义抽象类的时候需要用到关键字 `abstract`,放在 `class` 关键字前,就像下面这样。 + +```java +abstract class AbstractPlayer { +} +``` + +关于抽象类的命名,《阿里的 Java 开发手册》上有强调,“抽象类命名要使用 Abstract 或 Base 开头”,这条规约还是值得遵守的。 + +抽象类是不能实例化的,尝试通过 `new` 关键字实例化的话,编译器会报错,提示“类是抽象的,不能实例化”。 + +![](https://cdn.jsdelivr.net/gh/itwanger/Tech-Sister-Learn-Java/images/object-class/abstract-01.png) + +虽然抽象类不能实例化,但可以有子类。子类通过 `extends` 关键字来继承抽象类。就像下面这样。 + +```java +public class BasketballPlayer extends AbstractPlayer { +} +``` + +如果一个类定义了一个或多个抽象方法,那么这个类必须是抽象类。 + +当我们尝试在一个普通类中定义抽象方法的时候,编译器会有两处错误提示。第一处在类级别上,提示“这个类必须通过 `abstract` 关键字定义”,见下图。 + +![](https://cdn.jsdelivr.net/gh/itwanger/Tech-Sister-Learn-Java/images/object-class/abstract-02.png) + +第二处在尝试定义 abstract 的方法上,提示“抽象方法所在的类不是抽象的”,见下图。 + +![](https://cdn.jsdelivr.net/gh/itwanger/Tech-Sister-Learn-Java/images/object-class/abstract-03.png) + +抽象类中既可以定义抽象方法,也可以定义普通方法,就像下面这样: + +```java +public abstract class AbstractPlayer { + abstract void play(); + + public void sleep() { + System.out.println("运动员也要休息而不是挑战极限"); + } +} +``` + +抽象类派生的子类必须实现父类中定义的抽象方法。比如说,抽象类 AbstractPlayer 中定义了 `play()` 方法,子类 BasketballPlayer 中就必须实现。 + +```java +public class BasketballPlayer extends AbstractPlayer { + @Override + void play() { + System.out.println("我是张伯伦,篮球场上得过 100 分"); + } +} +``` + +如果没有实现的话,编译器会提示“子类必须实现抽象方法”,见下图。 + +![](https://cdn.jsdelivr.net/gh/itwanger/Tech-Sister-Learn-Java/images/object-class/abstract-04.png) + +“二哥,抽象方法我明白了,那什么时候使用抽象方法呢?能给我讲讲它的应用场景吗?”三妹及时的插话道。 + +“这问题问的恰到好处呀!”我扶了扶眼镜继续说。 + +**第一种场景**。 + +当我们希望一些通用的功能被多个子类复用的时候,就可以使用抽象类。比如说,AbstractPlayer 抽象类中有一个普通的方法 `sleep()`,表明所有运动员都需要休息,那么这个方法就可以被子类复用。 + +```java +abstract class AbstractPlayer { + public void sleep() { + System.out.println("运动员也要休息而不是挑战极限"); + } +} +``` + +子类 BasketballPlayer 继承了 AbstractPlayer 类: + +```java +class BasketballPlayer extends AbstractPlayer { +} +``` + +也就拥有了 `sleep()` 方法。BasketballPlayer 的对象可以直接调用父类的 `sleep()` 方法: + +```java +BasketballPlayer basketballPlayer = new BasketballPlayer(); +basketballPlayer.sleep(); +``` + +子类 FootballPlayer 继承了 AbstractPlayer 类: + +```java +class FootballPlayer extends AbstractPlayer { +} +``` + +也拥有了 `sleep()` 方法,FootballPlayer 的对象也可以直接调用父类的 `sleep()` 方法: + +```java +FootballPlayer footballPlayer = new FootballPlayer(); +footballPlayer.sleep(); +``` + +这样是不是就实现了代码的复用呢? + +**第二种场景**。 + +当我们需要在抽象类中定义好 API,然后在子类中扩展实现的时候就可以使用抽象类。比如说,AbstractPlayer 抽象类中定义了一个抽象方法 `play()`,表明所有运动员都可以从事某项运动,但需要对应子类去扩展实现,表明篮球运动员打篮球,足球运动员踢足球。 + +```java +abstract class AbstractPlayer { + abstract void play(); +} +``` + +BasketballPlayer 继承了 AbstractPlayer 类,扩展实现了自己的 `play()` 方法。 + +```java +public class BasketballPlayer extends AbstractPlayer { + @Override + void play() { + System.out.println("我是张伯伦,我篮球场上得过 100 分,"); + } +} +``` + +FootballPlayer 继承了 AbstractPlayer 类,扩展实现了自己的 `play()` 方法。 + +```java +public class FootballPlayer extends AbstractPlayer { + @Override + void play() { + System.out.println("我是C罗,我能接住任意高度的头球"); + } +} +``` + +为了进一步展示抽象类的特性,我们再来看一个具体的示例。假设现在有一个文件,里面的内容非常简单,只有一个“Hello World”,现在需要有一个读取器将内容从文件中读取出来,最好能按照大写的方式,或者小写的方式来读。 + +这时候,最好定义一个抽象类 BaseFileReader: + +```java +abstract class BaseFileReader { + protected Path filePath; + + protected BaseFileReader(Path filePath) { + this.filePath = filePath; + } + + public List readFile() throws IOException { + return Files.lines(filePath) + .map(this::mapFileLine).collect(Collectors.toList()); + } + + protected abstract String mapFileLine(String line); +} +``` + +- filePath 为文件路径,使用 protected 修饰,表明该成员变量可以在需要时被子类访问到。 + +- `readFile()` 方法用来读取文件,方法体里面调用了抽象方法 `mapFileLine()`——需要子类来扩展实现大小写的不同读取方式。 + +在我看来,BaseFileReader 类设计的就非常合理,并且易于扩展,子类只需要专注于具体的大小写实现方式就可以了。 + +小写的方式: + +```java +class LowercaseFileReader extends BaseFileReader { + protected LowercaseFileReader(Path filePath) { + super(filePath); + } + + @Override + protected String mapFileLine(String line) { + return line.toLowerCase(); + } +} +``` + +大写的方式: + +```java +class UppercaseFileReader extends BaseFileReader { + protected UppercaseFileReader(Path filePath) { + super(filePath); + } + + @Override + protected String mapFileLine(String line) { + return line.toUpperCase(); + } +} +``` + +从文件里面一行一行读取内容的代码被子类复用了。与此同时,子类只需要专注于自己该做的工作,LowercaseFileReader 以小写的方式读取文件内容,UppercaseFileReader 以大写的方式读取文件内容。 + +来看一下测试类 FileReaderTest: + +```java +public class FileReaderTest { + public static void main(String[] args) throws URISyntaxException, IOException { + URL location = FileReaderTest.class.getClassLoader().getResource("helloworld.txt"); + Path path = Paths.get(location.toURI()); + BaseFileReader lowercaseFileReader = new LowercaseFileReader(path); + BaseFileReader uppercaseFileReader = new UppercaseFileReader(path); + System.out.println(lowercaseFileReader.readFile()); + System.out.println(uppercaseFileReader.readFile()); + } +} +``` + +在项目的 resource 目录下建一个文本文件,名字叫 helloworld.txt,里面的内容就是“Hello World”。文件的具体位置如下图所示,我用的集成开发环境是 Intellij IDEA。 + +![](https://cdn.jsdelivr.net/gh/itwanger/Tech-Sister-Learn-Java/images/object-class/abstract-05.png) + + +在 resource 目录下的文件可以通过 `ClassLoader.getResource()` 的方式获取到 URI 路径,然后就可以取到文本内容了。 + +输出结果如下所示: + +``` +[hello world] +[HELLO WORLD] +``` + +------- + +“完了吗?二哥”三妹似乎还沉浸在聆听教诲的快乐中。 + +“是滴,这次我们系统化的学习了抽象类,可以说面面俱到了。三妹你可以把代码敲一遍,加强了一些印象,电脑交给你了。”说完,我就跑到阳台去抽烟了。 + +“呼。。。。。”一个大大的眼圈飘散开来,又是愉快的一天~ diff --git a/docs/object-class/java-interface.md b/docs/object-class/java-interface.md new file mode 100644 index 0000000000000000000000000000000000000000..13a38747e7f041997001ef660115ed48f24399d6 --- /dev/null +++ b/docs/object-class/java-interface.md @@ -0,0 +1,321 @@ +## 接口 + +“哥,我看你朋友圈说《教妹学 Java》专栏收到了第一笔赞赏呀,虽然只有一块钱,但我也替你感到开心。”三妹的脸上洋溢着自信的微笑,仿佛这钱是打给她的一样。 + +![](https://cdn.jsdelivr.net/gh/itwanger/Tech-Sister-Learn-Java/images/object-class/interface-01.png) + +“是啊,早上起来的时候看到这条信息,还真的是挺开心的,虽然只有一块钱,但是开源的第一笔,也是我人生当中的第一笔,真的非常感谢这个读者,值得纪念的一天。”我自己也掩饰不住内心的激动。 + +“有了这份鼓励,我相信你更新下去的动力更足了!”三妹今天说的话真的是特别令人喜欢。 + + +![](https://cdn.jsdelivr.net/gh/itwanger/Tech-Sister-Learn-Java/images/object-class/interface-02.png) + +“是啊是啊,所以,今天要更新第 26 讲了——接口。”我接着说,“对于面向对象编程来说,抽象是一个极具魅力的特征。如果一个程序员的抽象思维很差,那他在编程中就会遇到很多困难,无法把业务变成具体的代码。在 Java 中,可以通过两种形式来达到抽象的目的,一种上一篇的主角——[抽象类](https://mp.weixin.qq.com/s/WSmGwdtlimIFVVDVKfvrWQ),另外一种就是今天的主角——接口。” + +---------- + +“接口是什么呀?”三妹顺着我的话题及时的插话到。 + +接口通过 interface 关键字来定义,它可以包含一些常量和方法,来看下面这个示例。 + +```java +public interface Electronic { + // 常量 + String LED = "LED"; + + // 抽象方法 + int getElectricityUse(); + + // 静态方法 + static boolean isEnergyEfficient(String electtronicType) { + return electtronicType.equals(LED); + } + + // 默认方法 + default void printDescription() { + System.out.println("电子"); + } +} +``` + +来看一下这段代码反编译后的字节码。 + +```java +public interface Electronic +{ + + public abstract int getElectricityUse(); + + public static boolean isEnergyEfficient(String electtronicType) + { + return electtronicType.equals("LED"); + } + + public void printDescription() + { + System.out.println("\u7535\u5B50"); + } + + public static final String LED = "LED"; +} +``` + +发现没?接口中定义的所有变量或者方法,都会自动添加上 `public` 关键字。 + +接下来,我来一一解释下 Electronic 接口中的核心知识点。 + +1)接口中定义的变量会在编译的时候自动加上 `public static final` 修饰符(注意看一下反编译后的字节码),也就是说上例中的 LED 变量其实就是一个常量。 + +Java 官方文档上有这样的声明: + +>Every field declaration in the body of an interface is implicitly public, static, and final. + +换句话说,接口可以用来作为常量类使用,还能省略掉 `public static final`,看似不错的一种选择,对吧? + +不过,这种选择并不可取。因为接口的本意是对方法进行抽象,而常量接口会对子类中的变量造成命名空间上的“污染”。 + +2)没有使用 `private`、`default` 或者 `static` 关键字修饰的方法是隐式抽象的,在编译的时候会自动加上 `public abstract` 修饰符。也就是说上例中的 `getElectricityUse()` 其实是一个抽象方法,没有方法体——这是定义接口的本意。 + +3)从 Java 8 开始,接口中允许有静态方法,比如说上例中的 `isEnergyEfficient()` 方法。 + +静态方法无法由(实现了该接口的)类的对象调用,它只能通过接口名来调用,比如说 `Electronic.isEnergyEfficient("LED")`。 + +接口中定义静态方法的目的是为了提供一种简单的机制,使我们不必创建对象就能调用方法,从而提高接口的竞争力。 + +4)接口中允许定义 `default` 方法也是从 Java 8 开始的,比如说上例中的 `printDescription()` 方法,它始终由一个代码块组成,为,实现该接口而不覆盖该方法的类提供默认实现。既然要提供默认实现,就要有方法体,换句话说,默认方法后面不能直接使用“;”号来结束——编译器会报错。 + +![](https://cdn.jsdelivr.net/gh/itwanger/Tech-Sister-Learn-Java/images/object-class/interface-03.png) + +“为什么要在接口中定义默认方法呢?”三妹好奇地问到。 + +允许在接口中定义默认方法的理由很充分,因为一个接口可能有多个实现类,这些类就必须实现接口中定义的抽象类,否则编译器就会报错。假如我们需要在所有的实现类中追加某个具体的方法,在没有 `default` 方法的帮助下,我们就必须挨个对实现类进行修改。 + +由之前的例子我们就可以得出下面这些结论: + +- 接口中允许定义变量 +- 接口中允许定义抽象方法 +- 接口中允许定义静态方法(Java 8 之后) +- 接口中允许定义默认方法(Java 8 之后) + +除此之外,我们还应该知道: + +1)接口不允许直接实例化,否则编译器会报错。 + +![](https://cdn.jsdelivr.net/gh/itwanger/Tech-Sister-Learn-Java/images/object-class/interface-04.png) + +需要定义一个类去实现接口,见下例。 + +```java +public class Computer implements Electronic { + + public static void main(String[] args) { + new Computer(); + } + + @Override + public int getElectricityUse() { + return 0; + } +} +``` + +然后再实例化。 + +``` +Electronic e = new Computer(); +``` + +2)接口可以是空的,既可以不定义变量,也可以不定义方法。最典型的例子就是 Serializable 接口,在 `java.io` 包下。 + +```java +public interface Serializable { +} +``` + +Serializable 接口用来为序列化的具体实现提供一个标记,也就是说,只要某个类实现了 Serializable 接口,那么它就可以用来序列化了。 + +3)不要在定义接口的时候使用 final 关键字,否则会报编译错误,因为接口就是为了让子类实现的,而 final 阻止了这种行为。 + +![](https://cdn.jsdelivr.net/gh/itwanger/Tech-Sister-Learn-Java/images/object-class/interface-05.png) + +4)接口的抽象方法不能是 private、protected 或者 final,否则编译器都会报错。 + +![](https://cdn.jsdelivr.net/gh/itwanger/Tech-Sister-Learn-Java/images/object-class/interface-06.png) + +5)接口的变量是隐式 `public static final`(常量),所以其值无法改变。 + +“接口可以做什么呢?”三妹见缝插针,问的很及时。 + +第一,使某些实现类具有我们想要的功能,比如说,实现了 Cloneable 接口的类具有拷贝的功能,实现了 Comparable 或者 Comparator 的类具有比较功能。 + +Cloneable 和 Serializable 一样,都属于标记型接口,它们内部都是空的。实现了 Cloneable 接口的类可以使用 `Object.clone()` 方法,否则会抛出 CloneNotSupportedException。 + +```java +public class CloneableTest implements Cloneable { + @Override + protected Object clone() throws CloneNotSupportedException { + return super.clone(); + } + + public static void main(String[] args) throws CloneNotSupportedException { + CloneableTest c1 = new CloneableTest(); + CloneableTest c2 = (CloneableTest) c1.clone(); + } +} +``` + +运行后没有报错。现在把 `implements Cloneable` 去掉。 + +```java +public class CloneableTest { + @Override + protected Object clone() throws CloneNotSupportedException { + return super.clone(); + } + + public static void main(String[] args) throws CloneNotSupportedException { + CloneableTest c1 = new CloneableTest(); + CloneableTest c2 = (CloneableTest) c1.clone(); + + } +} +``` + +运行后抛出 CloneNotSupportedException: + +``` +Exception in thread "main" java.lang.CloneNotSupportedException: com.cmower.baeldung.interface1.CloneableTest + at java.base/java.lang.Object.clone(Native Method) + at com.cmower.baeldung.interface1.CloneableTest.clone(CloneableTest.java:6) + at com.cmower.baeldung.interface1.CloneableTest.main(CloneableTest.java:11) +``` + + +第二,Java 原则上只支持单一继承,但通过接口可以实现多重继承的目的。 + +如果有两个类共同继承(extends)一个父类,那么父类的方法就会被两个子类重写。然后,如果有一个新类同时继承了这两个子类,那么在调用重写方法的时候,编译器就不能识别要调用哪个类的方法了。这也正是著名的菱形问题,见下图。 + +![](https://cdn.jsdelivr.net/gh/itwanger/Tech-Sister-Learn-Java/images/object-class/interface-07.png) + + +简单解释下,ClassC 同时继承了 ClassA 和 ClassB,ClassC 的对象在调用 ClassA 和 ClassB 中重写的方法时,就不知道该调用 ClassA 的方法,还是 ClassB 的方法。 + +接口没有这方面的困扰。来定义两个接口,Fly 接口会飞,Run 接口会跑。 + +```java +public interface Fly { + void fly(); +} +public interface Run { + void run(); +} +``` + +然后让 Pig 类同时实现这两个接口。 + +```java +public class Pig implements Fly,Run{ + @Override + public void fly() { + System.out.println("会飞的猪"); + } + + @Override + public void run() { + System.out.println("会跑的猪"); + } +} +``` + +在某种形式上,接口实现了多重继承的目的:现实世界里,猪的确只会跑,但在雷军的眼里,站在风口的猪就会飞,这就需要赋予这只猪更多的能力,通过抽象类是无法实现的,只能通过接口。 + +第三,实现多态。 + +什么是多态呢?通俗的理解,就是同一个事件发生在不同的对象上会产生不同的结果,鼠标左键点击窗口上的 X 号可以关闭窗口,点击超链接却可以打开新的网页。 + +多态可以通过继承(`extends`)的关系实现,也可以通过接口的形式实现。 + +Shape 接口表示一个形状。 + +```java +public interface Shape { + String name(); +} +``` + +Circle 类实现了 Shape 接口,并重写了 `name()` 方法。 + +```java +public class Circle implements Shape { + @Override + public String name() { + return "圆"; + } +} +``` + +Square 类也实现了 Shape 接口,并重写了 `name()` 方法。 + +```java +public class Square implements Shape { + @Override + public String name() { + return "正方形"; + } +} +``` + +然后来看测试类。 + +```java +List shapes = new ArrayList<>(); +Shape circleShape = new Circle(); +Shape squareShape = new Square(); + +shapes.add(circleShape); +shapes.add(squareShape); + +for (Shape shape : shapes) { + System.out.println(shape.name()); +} +``` + +这就实现了多态,变量 circleShape、squareShape 的引用类型都是 Shape,但执行 `shape.name()` 方法的时候,Java 虚拟机知道该去调用 Circle 的 `name()` 方法还是 Square 的 `name()` 方法。 + +说一下多态存在的 3 个前提: + +1、要有继承关系,比如说 Circle 和 Square 都实现了 Shape 接口。 +2、子类要重写父类的方法,Circle 和 Square 都重写了 `name()` 方法。 +3、父类引用指向子类对象,circleShape 和 squareShape 的类型都为 Shape,但前者指向的是 Circle 对象,后者指向的是 Square 对象。 + +然后,我们来看一下测试结果: + +``` +圆 +正方形 +``` + +也就意味着,尽管在 for 循环中,shape 的类型都为 Shape,但在调用 `name()` 方法的时候,它知道 Circle 对象应该调用 Circle 类的 `name()` 方法,Square 对象应该调用 Square 类的 `name()` 方法。 + +“哦,我理解了哥。那我再问一下,抽象类和接口有什么差别呢?” + +“哇,三妹呀,你这个问题恰到好处,问到了点子上。”我不由得为三妹竖起了大拇指。 + +1)语法层面上 + +- 接口中不能有 public 和 protected 修饰的方法,抽象类中可以有。 +- 接口中的变量只能是隐式的常量,抽象类中可以有任意类型的变量。 +- 一个类只能继承一个抽象类,但却可以实现多个接口。 + +2)设计层面上 + +抽象类是对类的一种抽象,继承抽象类的子类和抽象类本身是一种 `is-a` 的关系。 + +接口是对类的某种行为的一种抽象,接口和类之间并没有很强的关联关系,举个例子来说,所有的类都可以实现 `Serializable` 接口,从而具有序列化的功能,但不能说所有的类和 Serializable 之间是 `is-a` 的关系。 + +-------- + +“好了,三妹,接口就学到这吧,下课,哈哈哈。”我抬起头看了看窗外,天气还真不错,希望五一的张家界也能晴空万里~ + +“嗯嗯,哥,休息下吧,我给你揉揉肩膀~~~~”不得不说,有个贴心的妹妹还真的是挺舒服。。。。。 \ No newline at end of file diff --git a/docs/object-class/pass-by-value.md b/docs/object-class/pass-by-value.md new file mode 100644 index 0000000000000000000000000000000000000000..29da0e1c9f95821dc6e848f34bebf240ed7206ce --- /dev/null +++ b/docs/object-class/pass-by-value.md @@ -0,0 +1,135 @@ +## 值传递与引用传递 + +“哥,说说 Java 到底是值传递还是引用传递吧?”三妹一脸的困惑,看得出来她被这个问题折磨得不轻。 + +“说实在的,我在一开始学 Java 的时候也被这个问题折磨得够呛,总以为基本数据类型在传参的时候是值传递,而引用类型是引用传递。”我对三妹袒露了心声,为的就是让她不再那么焦虑,她哥当年也是这么过来的。 + + C 语言是很多编程语言的母胎,包括 Java,那么对于 C 语言来说,所有的方法参数都是“通过值”传递的,也就是说,传递给被调用方法的参数值存放在临时变量中,而不是存放在原来的变量中。这就意味着,被调用的方法不能修改调用方法中变量的值,而只能修改其私有变量的临时副本的值。 + +Java 继承了 C 语言这一特性,因此 Java 是按照值来传递的。 + +接下来,我们必须得搞清楚,到底什么是值传递(pass by value),什么是引用传递(pass by reference),否则,讨论 Java 到底是值传递还是引用传递就显得毫无意义。 + +当一个参数按照值的方式在两个方法之间传递时,调用者和被调用者其实是用的两个不同的变量——被调用者中的变量(原始值)是调用者中变量的一份拷贝,对它们当中的任何一个变量修改都不会影响到另外一个变量,据说 Fortran 语言是通过引用传递的。 + +“Fortran 语言?”三妹睁大了双眼,似乎听见了什么新的名词。 + +“是的,Fortran 语言,1957 年由 IBM 公司开发,是世界上第一个被正式采用并流传至今的高级编程语言。” + +当一个参数按照引用传递的方式在两个方法之间传递时,调用者和被调用者其实用的是同一个变量,当该变量被修改时,双方都是可见的。 + +“我们之所以容易搞不清楚 Java 到底是值传递还是引用传递,主要是因为 Java 中的两类数据类型的叫法容易引发误会,比如说 int 是基本类型,说它是值传递的,我们就很容易理解;但对于引用类型,比如说 String,说它也是值传递的时候,我们就容易弄不明白。” + +我们来看看基本数据类型和引用数据类型之间的差别。 + +```java +int age = 18; +String name = "二哥"; +``` + +age 是基本类型,值就保存在变量中,而 name 是引用类型,变量中保存的是对象的地址。一般称这种变量为对象的引用,引用存放在栈中,而对象存放在堆中。 + +这里说的栈和堆,是指内存中的一块区域,和数据结构中的栈和堆不一样。栈是由编译器自动分配释放的,所以适合存放编译期就确定生命周期的数据;而堆中存放的数据,编译器是不需要知道生命周期的,创建后的回收工作由垃圾收集器来完成。 + +“画幅图。” + +![](https://cdn.jsdelivr.net/gh/itwanger/Tech-Sister-Learn-Java/images/object-class/pass-by-value-01.png) + +当用 = 赋值运算符改变 age 和 name 的值时。 + +```java +age = 16; +name = "三妹"; +``` + +对于基本类型 age,赋值运算符会直接改变变量的值,原来的值被覆盖。 + +对于引用类型 name,赋值运算符会改变对象引用中保存的地址,原来的地址被覆盖,但原来的对象不会被覆盖。 + +![](https://cdn.jsdelivr.net/gh/itwanger/Tech-Sister-Learn-Java/images/object-class/pass-by-value-02.png) + +“三妹,注意听,接下来,我们来说说基本数据类型的参数传递。” + +Java 有 8 种基本数据类型,分别是 int、long、byte、short、float、double 、char 和 boolean,就拿 int 类型来举例吧。 + +```java +class PrimitiveTypeDemo { + public static void main(String[] args) { + int age = 18; + modify(age); + System.out.println(age); + } + + private static void modify(int age1) { + age1 = 30; + } +} +``` + +1)`main()` 方法中的 age 为基本类型,所以它的值 18 直接存储在变量中。 + +2)调用 `modify()` 方法的时候,将会把 age 的值 18 复制给形参 age1。 + +3)`modify()` 方法中,对 age1 做出了修改。 + +4)回到 `main()` 方法中,age 的值仍然为 18,并没有发生改变。 + +如果我们想让 age 的值发生改变,就需要这样做。 + +```java +class PrimitiveTypeDemo1 { + public static void main(String[] args) { + int age = 18; + age = modify(age); + System.out.println(age); + } + + private static int modify(int age1) { + age1 = 30; + return age1; + } +} +``` + +第一,让 `modify()` 方法有返回值; + +第二,使用赋值运算符重新对 age 进行赋值。 + +“好了,再来说说引用类型的参数传递。” + +就以 String 为例吧。 + +```java +class ReferenceTypeDemo { + public static void main(String[] args) { + String name = "二哥"; + modify(name); + System.out.println(name); + } + + private static void modify(String name1) { + name1 = "三妹"; + } +} +``` + +在调用 `modify()` 方法的时候,形参 name1 复制了 name 的地址,指向的是堆中“二哥”的位置。 + +![](https://cdn.jsdelivr.net/gh/itwanger/Tech-Sister-Learn-Java/images/object-class/pass-by-value-03.png) + +当 `modify()` 方法调用结束后,改变了形参 name1 的地址,但 `main()` 方法中 name 并没有发生改变。 + +![](https://cdn.jsdelivr.net/gh/itwanger/Tech-Sister-Learn-Java/images/object-class/pass-by-value-04.png) + +总结: + +- Java 中的参数传递是按值传递的。 +- 如果参数是基本类型,传递的是基本类型的字面量值的拷贝。 +- 如果参数是引用类型,传递的是引用的对象在堆中地址的拷贝。 + +“好了,三妹,今天的学习就到这吧。” + + + + + diff --git a/docs/string/constant-pool.md b/docs/string/constant-pool.md new file mode 100644 index 0000000000000000000000000000000000000000..bc56c18c48975d6c7fee5a178164f95b7e20bd29 --- /dev/null +++ b/docs/string/constant-pool.md @@ -0,0 +1,86 @@ +## 字符串常量池 + +“三妹,今天我们来学习一下字符串常量池吧,这是字符串非常中关键的一个知识点。”我话音未落,青岛路小学那边传来了嘹亮的歌声就钻进了我的耳朵,“唱 ~ 山 ~ 歌 ~” + +三妹说,“好呀,开始吧,哥。” + +“先从这道面试题开始吧!” + +```java +String s = new String("二哥"); +``` + +“这行代码创建了几个对象?” + +“不就一个吗?”三妹不假思索地回答。 + +“不,两个!”我直接否定了三妹的答案,“使用 new 关键字创建一个字符串对象时,Java 虚拟机会先在字符串常量池中查找有没有‘二哥’这个字符串对象,如果有,就不会在字符串常量池中创建‘二哥’这个对象了,直接在堆中创建一个‘二哥’的字符串对象,然后将堆中这个‘二哥’的对象地址返回赋值给变量 s。” + +“如果没有,先在字符串常量池中创建一个‘二哥’的字符串对象,然后再在堆中创建一个‘二哥’的字符串对象,然后将堆中这个‘二哥’的字符串对象地址返回赋值给变量 s。” + +“为什么要先在字符串常量池中创建对象,然后再在堆上创建呢?这样不就多此一举了?”三妹敏锐地发现了问题。 + +我回答,“由于字符串的使用频率实在是太高了,所以 Java 虚拟机为了提高性能和减少内存开销,在创建字符串对象的时候进行了一些优化,特意为字符串开辟了一个字符串常量池。” + +通常情况下,我们会采用双引号的方式来创建字符串对象,而不是通过 new 关键字的方式: + +```java +String s = "三妹"; +``` + +当执行 `String s = "三妹"` 时,Java 虚拟机会先在字符串常量池中查找有没有“三妹”这个字符串对象,如果有,则不创建任何对象,直接将字符串常量池中这个“三妹”的对象地址返回,赋给变量 s;如果没有,在字符串常量池中创建“三妹”这个对象,然后将其地址返回,赋给变量 s。 + +“哦,我明白了,哥。”三妹突然插话到,“有了字符串常量池,就可以通过双引号的方式直接创建字符串对象,不用再通过 new 的方式在堆中创建对象了,对吧?” + +“是滴。new 的方式始终会创建一个对象,不管字符串的内容是否已经存在,而双引号的方式会重复利用字符串常量池中已经存在的对象。”我说。 + +来看下面这个例子: + +```java +String s = new String("二哥"); +String s1 = new String("二哥"); +``` + + 按照我们之前的分析,这两行代码会创建三个对象,字符串常量池中一个,堆上两个。 + +再来看下面这个例子: + +```java +String s = "三妹"; +String s1 = "三妹"; +``` + +这两行代码只会创建一个对象,就是字符串常量池中的那个。这样的话,性能肯定就提高了! + +“那哥,字符串常量池在内存中的什么位置呢?”三妹问。 + +我说,“三妹,你这个问题问得好呀!” + +在 Java 8 之前,字符串常量池在永久代中。 + +![](https://cdn.jsdelivr.net/gh/itwanger/Tech-Sister-Learn-Java/images/string/constant-pool-01.png) + +Java 8 之后,移除了永久代,字符串常量池就移到了堆中。 + +![](https://cdn.jsdelivr.net/gh/itwanger/Tech-Sister-Learn-Java/images/string/constant-pool-02.png) + +“哥,能再简单给我解释一下方法区,永久代和元空间的概念吗?有点模糊。”三妹说。 + +我说,“可以呀。” + +- 方法区是 Java 虚拟机规范中的一个概念,就像是一个接口吧; +- 永久代是 HotSpot 虚拟机中对方法的一个实现,就像是接口的实现类; +- Java 8 的时候,移除了永久代,取而代之的是元空间,是方法区的另外一个实现。 + +永久代是放在运行时数据区中的,所以它的大小受到 Java 虚拟机本身大小的限制,所以 Java 8 之前,会经常遇到 `java.lang.OutOfMemoryError: PremGen Space` 的异常,PremGen Space 就是方法区的意思;而元空间是直接放在内存中的,所以只受本机可用内存的限制,虽然也会发生内存溢出,但出现的几率相对之前就小了很多。 + +“明白了吧,三妹?”我问。 + +“嗯嗯。”三妹回答。 + +“那关于字符串常量池,就先说这么多吧,是不是还挺有意思的。”我说。 + +“是的,我现在是彻底搞懂了字符串常量池,哥,你真棒!”三妹说。 + + +PS:点击「**阅读原文**」可直达《教妹学Java》专栏的 GitHub 开源地址,记得 star 哦! \ No newline at end of file diff --git a/docs/string/intern.md b/docs/string/intern.md new file mode 100644 index 0000000000000000000000000000000000000000..586c3be746b0b3babd571dc7ebc71aecf9f571a2 --- /dev/null +++ b/docs/string/intern.md @@ -0,0 +1,109 @@ +## intern + +“哥,你发给我的那篇文章我看了,结果直接把我给看得不想学 Java 了!”三妹气冲冲地说。 + +“哪一篇啊?”看着三妹面色沉重,我关心地问到。 + +“就是美团技术团队深入解析 `String.intern()` 那篇啊!”三妹回答。 + +>https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html + +“哦,我想起来了,不挺好一篇文章嘛,深入浅出,精品中的精品,看完后你应该对 String 的 intern 彻底理解了才对呀。” + +“好是好,但我就是看不懂!”三妹委屈地说,“哥,还是你亲自给我讲讲吧?” + +“好吧,上次学的[字符串常量池](https://mp.weixin.qq.com/s/b69zXknKLIa3FWs0Yj23xA)你都搞清楚了吧?” + +“嗯。”三妹微微的点了点头。 + +要理解美团技术团队的这篇文章,你只需要记住这几点内容: + +第一,使用双引号声明的字符串对象会保存在字符串常量池中。 + +第二,使用 new 关键字创建的字符串对象会先从字符串常量池中找,如果没找到就创建一个,然后再在堆中创建字符串对象;如果找到了,就直接在堆中创建字符串对象。 + +第三,针对没有使用双引号声明的字符串对象来说,就像下面代码中的 s1 那样: + +```java +String s1 = new String("二哥") + new String("三妹"); +``` + +如果想把 s1 的内容也放入字符串常量池的话,可以调用 `intern()` 方法来完成。 + +不过,需要注意的是,Java 7 的时候,字符串常量池从永久代中移动到了堆中,虽然此时永久代还没有完全被移除。Java 8 的时候,永久代被彻底移除。 + +这个变化也直接影响了 `String.intern()` 方法在执行时的策略,Java 7 之前,执行 `String.intern()` 方法的时候,不管对象在堆中是否已经创建,字符串常量池中仍然会创建一个内容完全相同的新对象; Java 7 之后呢,由于字符串常量池放在了堆中,执行 `String.intern()` 方法的时候,如果对象在堆中已经创建了,字符串常量池中就不需要再创建新的对象了,而是直接保存堆中对象的引用,也就节省了一部分的内存空间。 + +“三妹,来猜猜这段代码输出的结果吧。”我说。 + +```java +String s1 = new String("二哥三妹"); +String s2 = s1.intern(); +System.out.println(s1 == s2); +``` + +“哥,这我完全猜不出啊,还是你直接解释吧。”三妹说。 + +“好吧。” + +第一行代码,字符串常量池中会先创建一个“二哥三妹”的对象,然后堆中会再创建一个“二哥三妹”的对象,s1 引用的是堆中的对象。 + +第二行代码,对 s1 执行 `intern()` 方法,该方法会从字符串常量池中查找“二哥三妹”这个字符串是否存在,此时是存在的,所以 s2 引用的是字符串常量池中的对象。 + +也就意味着 s1 和 s2 的引用地址是不同的,一个来自堆,一个来自字符串常量池,所以输出的结果为 false。 + +“来看一下运行结果。”我说。 + +``` +false +``` + +“我来画幅图,帮助你理解下。”看到三妹惊讶的表情,我耐心地说。 + +![](https://cdn.jsdelivr.net/gh/itwanger/Tech-Sister-Learn-Java/images/string/intern-01.png) + +“这下理解了吧?”我问三妹。 + +“嗯嗯,一下子就豁然开朗了!”三妹说。 + +“好,我们再来看下面这段代码。” + +```java +String s1 = new String("二哥") + new String("三妹"); +String s2 = s1.intern(); +System.out.println(s1 == s2); +``` + +“难道也输出 false ?”三妹有点不确定。 + +“不,这段代码会输出 true。”我否定了三妹的猜测。 + +“为啥呀?”三妹迫切地想要知道答案。 + +第一行代码,会在字符串常量池中创建两个对象,一个是“二哥”,一个是“三妹”,然后在堆中会创建两个匿名对象“二哥”和“三妹”(可以暂时忽略),最后还有一个“二哥三妹”的对象,s1 引用的是堆中“二哥三妹”这个对象。 + +第二行代码,对 s1 执行 `intern()` 方法,该方法会从字符串常量池中查找“二哥三妹”这个对象是否存在,此时不存在的,但堆中已经存在了,所以字符串常量池中保存的是堆中这个“二哥三妹”对象的引用,也就是说,s2 和 s1 的引用地址是相同的,所以输出的结果为 true。 + +“来看一下运行结果。”我胸有成竹地说。 + +``` +true +``` + +“我再来画幅图,帮助你理解下。” + +![](https://cdn.jsdelivr.net/gh/itwanger/Tech-Sister-Learn-Java/images/string/intern-02.png) + +“哇,我明白了!”三妹长舒一口气,大有感慨 intern 也没什么难理解的意味。 + +不过需要注意的是,尽管 intern 可以确保所有具有相同内容的字符串共享相同的内存空间,但也不要烂用 intern,因为任何的缓存池都是有大小限制的,不能无缘无故就占用了相对稀缺的缓存空间,导致其他字符串没有坑位可占。 + +另外,字符串常量池本质上是一个固定大小的 StringTable,如果放进去的字符串过多,就会造成严重的哈希冲突,从而导致链表变长,链表变长也就意味着字符串常量池的性能会大幅下降,因为要一个一个找是需要花费时间的。 + +“好了,三妹,关于 String 的 intern 就讲到这吧,这次理解了吧?”我问。 + +“哥,你真棒!” + +看到三妹一点一滴的进步,我也感到由衷的开心。 + +PS:点击「**阅读原文**」可直达《教妹学Java》专栏的 GitHub 开源地址,记得 star 哦! \ No newline at end of file diff --git a/docs/string/source.md b/docs/string/source.md new file mode 100644 index 0000000000000000000000000000000000000000..c4cbafa237186f5b1c9caad27471d1595d997718 --- /dev/null +++ b/docs/string/source.md @@ -0,0 +1,131 @@ +## 字符串源码分析 + +我正坐在沙发上津津有味地读刘欣大佬的《码农翻身》——Java 帝国这一章,门铃响了。起身打开门一看,是三妹,她从学校回来了。 + +“三妹,你回来的真及时,今天我们打算讲 Java 中的字符串呢。”等三妹换鞋的时候我说。 + +“哦,可以呀,哥。听说字符串的细节特别多,什么字符串常量池了、字符串不可变性了、字符串拼接了、字符串长度限制了等等,你最好慢慢讲,否则我可能一时半会消化不了。”三妹的态度显得很诚恳。 + +“嗯,我已经想好了,今天就只带你大概认识一下字符串,具体的细节咱们后面再慢慢讲,保证你能及时消化。” + +“好,那就开始吧。”三妹已经准备好坐在了电脑桌的边上。 + +我应了一声后走到电脑桌前坐下来,顺手打开 Intellij IDEA,并找到了 String 的源码。 + +```java +public final class String + implements java.io.Serializable, Comparable, CharSequence { + @Stable + private final byte[] value; + private final byte coder; + private int hash; +} +``` + +“第一,String 类是 final 的,意味着它不能被子类继承。” + +“第二,String 类实现了 Serializable 接口,意味着它可以序列化。” + +“第三,String 类实现了 Comparable 接口,意味着最好不要用‘==’来比较两个字符串是否相等,而应该用 `compareTo()` 方法去比较。” + +“第四,StringBuffer、StringBuilder 和 String 一样,都实现了 CharSequence 接口,所以它们仨属于近亲。由于 String 是不可变的,所以遇到字符串拼接的时候就可以考虑一下 String 的另外两个好兄弟,StringBuffer 和 StringBuilder,它俩是可变的。” + +“第五,Java 9 以前,String 是用 char 型数组实现的,之后改成了 byte 型数组实现,并增加了 coder 来表示编码。在 Latin1 字符为主的程序里,可以把 String 占用的内存减少一半。当然,天下没有免费的午餐,这个改进在节省内存的同时引入了编码检测的开销。” + +“第六,每一个字符串都会有一个 hash 值,这个哈希值在很大概率是不会重复的,因此 String 很适合来作为 HashMap 的键值。” + +“String 可能是 Java 中使用频率最高的引用类型了,因此 String 类的设计者可以说是用心良苦。” + +比如说 String 的不可变性。 + +- String 类被 final 关键字修饰,所以它不会有子类,这就意味着没有子类可以重写它的方法,改变它的行为。 +- String 类的数据存储在 `byte[]` 数组中,而这个数组也被 final 关键字修饰了,这就表示 String 对象是没法被修改的,只要初始化一次,值就确定了。 + +“哥,为什么要这样设计呢?”三妹有些不解。 + +“我先简单来说下,三妹,能懂最好,不能懂后面再细说。” + +第一,可以保证 String 对象的安全性,避免被篡改,毕竟像密码这种隐私信息一般就是用字符串存储的。 + +第二,保证哈希值不会频繁变更。毕竟要经常作为哈希表的键值,经常变更的话,哈希表的性能就会很差劲。 + +第三,可以实现字符串常量池。 + +“由于字符串的不可变性,String 类的一些方法实现最终都返回了新的字符串对象。”等三妹稍微缓了一会后,我继续说到。 + +“就拿 `substring()` 方法来说。” + +```java +public String substring(int beginIndex) { + if (beginIndex < 0) { + throw new StringIndexOutOfBoundsException(beginIndex); + } + int subLen = length() - beginIndex; + if (subLen < 0) { + throw new StringIndexOutOfBoundsException(subLen); + } + if (beginIndex == 0) { + return this; + } + return isLatin1() ? StringLatin1.newString(value, beginIndex, subLen) + : StringUTF16.newString(value, beginIndex, subLen); +} + +// StringLatin1.newString +public static String newString(byte[] val, int index, int len) { + return new String(Arrays.copyOfRange(val, index, index + len), + LATIN1); +} + +// UTF16.newString +public static String newString(byte[] val, int index, int len) { + if (String.COMPACT_STRINGS) { + byte[] buf = compress(val, index, len); + if (buf != null) { + return new String(buf, LATIN1); + } + } + int last = index + len; + return new String(Arrays.copyOfRange(val, index << 1, last << 1), UTF16); +} +``` + +`substring()` 方法用于截取字符串,不管是 Latin1 字符还是 UTF16 字符,最终返回的都是 new 出来的新字符串对象。 + +“还有 `concat()` 方法。” + +```java +public String concat(String str) { + int olen = str.length(); + if (olen == 0) { + return this; + } + if (coder() == str.coder()) { + byte[] val = this.value; + byte[] oval = str.value; + int len = val.length + oval.length; + byte[] buf = Arrays.copyOf(val, len); + System.arraycopy(oval, 0, buf, val.length, oval.length); + return new String(buf, coder); + } + int len = length(); + byte[] buf = StringUTF16.newBytesFor(len + olen); + getBytes(buf, 0, UTF16); + str.getBytes(buf, len, UTF16); + return new String(buf, UTF16); +} +``` + +`concat()` 方法用于拼接字符串,不管编码是否一致,最终也返回的是新的字符串对象。 + +“`replace()` 替换方法其实也一样,三妹,你可以自己一会看一下源码,也是返回新的字符串对象。” + +“这就意味着,不管是截取、拼接,还是替换,都不是在原有的字符串上进行的,而是重新生成了新的字符串对象。也就是说,这些操作执行过后,**原来的字符串对象并没有发生改变**。” + +“三妹,你记住,String 对象一旦被创建后就固定不变了,对 String 对象的任何修改都不会影响到原来的字符串对象,都会生成新的字符串对象。” + +“嗯嗯,记住了,哥。”三妹很乖。 + +“那今天就先讲到这吧,后面我们再对每一个细分领域深入地展开一下。你可以找一些资料先预习下,我出去散会心。。。。。” + +PS:点击「**阅读原文**」可直达《教妹学Java》专栏的 GitHub 开源地址,记得 star 哦! \ No newline at end of file diff --git a/images/array/print-01.png b/images/array/print-01.png new file mode 100644 index 0000000000000000000000000000000000000000..c1b4f87c4ee3a5febd01a812ca59c556060d1d1a Binary files /dev/null and b/images/array/print-01.png differ diff --git a/images/array/print-02.png b/images/array/print-02.png new file mode 100644 index 0000000000000000000000000000000000000000..543e1099fe8119a0a7877522a8f0b23259260b62 Binary files /dev/null and b/images/array/print-02.png differ diff --git a/images/array/print-03.png b/images/array/print-03.png new file mode 100644 index 0000000000000000000000000000000000000000..82672572459e1adff40a4d90d280dc5f7c2803d5 Binary files /dev/null and b/images/array/print-03.png differ diff --git a/images/object-class/abstract-01.png b/images/object-class/abstract-01.png new file mode 100644 index 0000000000000000000000000000000000000000..64fb4210917ac3763491b6c87de8320ba3063647 Binary files /dev/null and b/images/object-class/abstract-01.png differ diff --git a/images/object-class/abstract-02.png b/images/object-class/abstract-02.png new file mode 100644 index 0000000000000000000000000000000000000000..213ce81236580592879ad2028e7610dabf41ea04 Binary files /dev/null and b/images/object-class/abstract-02.png differ diff --git a/images/object-class/abstract-03.png b/images/object-class/abstract-03.png new file mode 100644 index 0000000000000000000000000000000000000000..dd2dc7fc5e4c8bc938ec3ba7ab4e524f3d6aef73 Binary files /dev/null and b/images/object-class/abstract-03.png differ diff --git a/images/object-class/abstract-04.png b/images/object-class/abstract-04.png new file mode 100644 index 0000000000000000000000000000000000000000..058e04016d27db3b19665e7f3296db7580540273 Binary files /dev/null and b/images/object-class/abstract-04.png differ diff --git a/images/object-class/abstract-05.png b/images/object-class/abstract-05.png new file mode 100644 index 0000000000000000000000000000000000000000..1fe4d7a7b30435e718990e4f63e85623a437119f Binary files /dev/null and b/images/object-class/abstract-05.png differ diff --git a/images/object-class/box-01.png b/images/object-class/box-01.png new file mode 100644 index 0000000000000000000000000000000000000000..f3bcde846810b2c89e7d823d6903cf4bf13a4d31 Binary files /dev/null and b/images/object-class/box-01.png differ diff --git a/images/object-class/deep-copy-01.png b/images/object-class/deep-copy-01.png new file mode 100644 index 0000000000000000000000000000000000000000..6cd9eb2e4d0e2f24fea33eed30bfce1e3e4ae556 Binary files /dev/null and b/images/object-class/deep-copy-01.png differ diff --git a/images/object-class/deep-copy-02.png b/images/object-class/deep-copy-02.png new file mode 100644 index 0000000000000000000000000000000000000000..a64ab815fbeef8cd7efcb1f925740623ae604807 Binary files /dev/null and b/images/object-class/deep-copy-02.png differ diff --git a/images/object-class/deep-copy-03.png b/images/object-class/deep-copy-03.png new file mode 100644 index 0000000000000000000000000000000000000000..848030d936f4495cd83243220030c08bdbc3a7d7 Binary files /dev/null and b/images/object-class/deep-copy-03.png differ diff --git a/images/object-class/interface-01.png b/images/object-class/interface-01.png new file mode 100644 index 0000000000000000000000000000000000000000..eea89f67ee65c3ef61fe765d040aed489f2ec881 Binary files /dev/null and b/images/object-class/interface-01.png differ diff --git a/images/object-class/interface-02.png b/images/object-class/interface-02.png new file mode 100644 index 0000000000000000000000000000000000000000..cc8d803d75728deb2431a126470902e794fbac01 Binary files /dev/null and b/images/object-class/interface-02.png differ diff --git a/images/object-class/interface-03.png b/images/object-class/interface-03.png new file mode 100644 index 0000000000000000000000000000000000000000..611a3479487c059f412fd77704ba02c2264f5612 Binary files /dev/null and b/images/object-class/interface-03.png differ diff --git a/images/object-class/interface-04.png b/images/object-class/interface-04.png new file mode 100644 index 0000000000000000000000000000000000000000..4c47b0cdd7bcd0ef48f367e90c92e68d9abedc30 Binary files /dev/null and b/images/object-class/interface-04.png differ diff --git a/images/object-class/interface-05.png b/images/object-class/interface-05.png new file mode 100644 index 0000000000000000000000000000000000000000..58bacad3e57f0d74e654fc651709ae86110ad1fc Binary files /dev/null and b/images/object-class/interface-05.png differ diff --git a/images/object-class/interface-06.png b/images/object-class/interface-06.png new file mode 100644 index 0000000000000000000000000000000000000000..4d26823a7651c42818635da313085b1c5fed683f Binary files /dev/null and b/images/object-class/interface-06.png differ diff --git a/images/object-class/interface-07.png b/images/object-class/interface-07.png new file mode 100644 index 0000000000000000000000000000000000000000..def25e8ae8a0ff048d8f1a4de705b4a50e4da9e8 Binary files /dev/null and b/images/object-class/interface-07.png differ diff --git a/images/object-class/pass-by-value-01.png b/images/object-class/pass-by-value-01.png new file mode 100644 index 0000000000000000000000000000000000000000..9725fdca6ce956b7fac5b8eb405b2c5a59ea198c Binary files /dev/null and b/images/object-class/pass-by-value-01.png differ diff --git a/images/object-class/pass-by-value-02.png b/images/object-class/pass-by-value-02.png new file mode 100644 index 0000000000000000000000000000000000000000..463475e1efb893454af9e05fc83b405966103b97 Binary files /dev/null and b/images/object-class/pass-by-value-02.png differ diff --git a/images/object-class/pass-by-value-03.png b/images/object-class/pass-by-value-03.png new file mode 100644 index 0000000000000000000000000000000000000000..eaed16da40cc6c5983eef23127817b95a2529b82 Binary files /dev/null and b/images/object-class/pass-by-value-03.png differ diff --git a/images/object-class/pass-by-value-04.png b/images/object-class/pass-by-value-04.png new file mode 100644 index 0000000000000000000000000000000000000000..41e1c15a3bc6252495a9f0f7a4360c8f750b0180 Binary files /dev/null and b/images/object-class/pass-by-value-04.png differ diff --git a/images/string/constant-pool-01.png b/images/string/constant-pool-01.png new file mode 100644 index 0000000000000000000000000000000000000000..b0596c39fa65df21f3db5709cc2b02ce46adb006 Binary files /dev/null and b/images/string/constant-pool-01.png differ diff --git a/images/string/constant-pool-02.png b/images/string/constant-pool-02.png new file mode 100644 index 0000000000000000000000000000000000000000..83ff6a6f42230473f6922915ac3bceac2f1f97c9 Binary files /dev/null and b/images/string/constant-pool-02.png differ diff --git a/images/string/intern-01.png b/images/string/intern-01.png new file mode 100644 index 0000000000000000000000000000000000000000..d07a9b79994780f603861daa8cb7a41f482f14b7 Binary files /dev/null and b/images/string/intern-01.png differ diff --git a/images/string/intern-02.png b/images/string/intern-02.png new file mode 100644 index 0000000000000000000000000000000000000000..8c2d35b06ffeed8b8f7440c75ec9b4c13dac0158 Binary files /dev/null and b/images/string/intern-02.png differ