From 215fa0e5f307ae93001c56cd57c7af1656041076 Mon Sep 17 00:00:00 2001 From: "hollis.zhl" Date: Sun, 20 Jun 2021 18:15:16 +0800 Subject: [PATCH] =?UTF-8?q?=E7=9F=A5=E8=AF=86=E7=82=B9=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + docs/basics/java-basic/constructor.md | 5 - docs/basics/java-basic/final-string.md | 114 ++++++++++++++---- .../java-basic/inheritance-composition.md | 67 ---------- docs/basics/java-basic/replace-in-string.md | 2 +- docs/basics/java-basic/scope.md | 7 -- docs/basics/java-basic/string-append.md | 2 + docs/basics/java-basic/substring.md | 4 +- docs/basics/java-basic/switch-string.md | 2 - docs/basics/java-basic/variable.md | 29 ----- .../basics/object-oriented/characteristics.md | 15 ++- docs/basics/object-oriented/constructor.md | 4 +- .../object-oriented/extends-implement.md | 19 +-- .../inheritance-composition.md | 2 +- docs/basics/object-oriented/java-pass-by.md | 101 +++++++++++----- .../object-oriented/multiple-inheritance.md | 2 +- .../object-oriented-vs-procedure-oriented.md | 38 +++--- .../overloading-vs-overriding.md | 106 ++++++---------- docs/basics/object-oriented/polymorphism.md | 52 ++++++-- docs/basics/object-oriented/principle.md | 46 +++++-- docs/basics/object-oriented/scope.md | 6 +- docs/basics/object-oriented/variable.md | 8 ++ .../object-oriented/why-pass-by-reference.md | 93 +------------- 23 files changed, 349 insertions(+), 376 deletions(-) delete mode 100644 docs/basics/java-basic/constructor.md delete mode 100644 docs/basics/java-basic/inheritance-composition.md delete mode 100644 docs/basics/java-basic/scope.md delete mode 100644 docs/basics/java-basic/variable.md diff --git a/.gitignore b/.gitignore index 5927d83f..3587059e 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,4 @@ fabric.properties # Editor-based Rest Client .idea/httpRequests +.DS_Store \ No newline at end of file diff --git a/docs/basics/java-basic/constructor.md b/docs/basics/java-basic/constructor.md deleted file mode 100644 index a0b3c011..00000000 --- a/docs/basics/java-basic/constructor.md +++ /dev/null @@ -1,5 +0,0 @@ -构造函数,是一种特殊的方法。 主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。 特别的一个类可以有多个构造函数,可根据其参数个数的不同或参数类型的不同来区分它们即构造函数的重载。 - -构造函数跟一般的实例方法十分相似;但是与其它方法不同,构造器没有返回类型,不会被继承,且可以有范围修饰符。构造器的函数名称必须和它所属的类的名称相同。 它承担着初始化对象数据成员的任务。 - -如果在编写一个可实例化的类时没有专门编写构造函数,多数编程语言会自动生成缺省构造器(默认构造函数)。默认构造函数一般会把成员变量的值初始化为默认值,如int -> 0,Integer -> null。 diff --git a/docs/basics/java-basic/final-string.md b/docs/basics/java-basic/final-string.md index 3f7aade7..920b8e0c 100644 --- a/docs/basics/java-basic/final-string.md +++ b/docs/basics/java-basic/final-string.md @@ -1,39 +1,109 @@ +String在Java中特别常用,而且我们经常要在代码中对字符串进行赋值和改变他的值,但是,为什么我们说字符串是不可变的呢? -* * * +首先,我们需要知道什么是不可变对象? -## 定义一个字符串 +不可变对象是在完全创建后其内部状态保持不变的对象。这意味着,一旦对象被赋值给变量,我们既不能更新引用,也不能通过任何方式改变内部状态。 - String s = "abcd"; - +可是有人会有疑惑,String为什么不可变,我的代码中经常改变String的值啊,如下: -![String-Immutability-1][1] +``` +String s = "abcd"; +s = s.concat("ef"); -`s`中保存了string对象的引用。下面的箭头可以理解为“存储他的引用”。 +``` -## 使用变量来赋值变量 - String s2 = s; - +这样,操作,不就将原本的"abcd"的字符串改变成"abcdef"了么? -![String-Immutability-2][2] +但是,虽然字符串内容看上去从"abcd"变成了"abcdef",但是实际上,我们得到的已经是一个新的字符串了。 -s2保存了相同的引用值,因为他们代表同一个对象。 +![][1] -## 字符串连接 +如上图,在堆中重新创建了一个"abcdef"字符串,和"abcd"并不是同一个对象。 - s = s.concat("ef"); - +所以,一旦一个string对象在内存(堆)中被创建出来,他就无法被修改。而且,String类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象。 -![string-immutability][3] +如果我们想要一个可秀改的字符串,可以选择StringBuffer 或者 StringBuilder这两个代替String。 -`s`中保存的是一个重新创建出来的string对象的引用。 +### 为什么String要设计成不可变 -## 总结 +在知道了"String是不可变"的之后,大家是不是一定都很疑惑:为什么要把String设计成不可变的呢?有什么好处呢? -一旦一个string对象在内存(堆)中被创建出来,他就无法被修改。特别要注意的是,String类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象。 +这个问题,困扰过很多人,甚至有人直接问过Java的创始人James Gosling。 -如果你需要一个可修改的字符串,应该使用StringBuffer 或者 StringBuilder。否则会有大量时间浪费在垃圾回收上,因为每次试图修改都有新的string对象被创建出来。 +在一次采访中James Gosling被问到什么时候应该使用不可变变量,他给出的回答是: - [1]: http://www.programcreek.com/wp-content/uploads/2009/02/String-Immutability-1.jpeg - [2]: http://www.programcreek.com/wp-content/uploads/2009/02/String-Immutability-2.jpeg - [3]: http://www.programcreek.com/wp-content/uploads/2009/02/string-immutability-650x279.jpeg \ No newline at end of file +> I would use an immutable whenever I can. + +那么,他给出这个答案背后的原因是什么呢?是基于哪些思考的呢? + +其实,主要是从缓存、安全性、线程安全和性能等角度触发的。 + +Q:缓存、安全性、线程安全和性能?这有都是啥 +A:你别急,听我一个一个给你讲就好了。 + +#### 缓存 + +字符串是使用最广泛的数据结构。大量的字符串的创建是非常耗费资源的,所以,Java提供了对字符串的缓存功能,可以大大的节省堆空间。 + +JVM中专门开辟了一部分空间来存储Java字符串,那就是字符串池。 + +通过字符串池,两个内容相同的字符串变量,可以从池中指向同一个字符串对象,从而节省了关键的内存资源。 + +``` +String s = "abcd"; +String s2 = s; +``` + + +对于这个例子,s和s2都表示"abcd",所以他们会指向字符串池中的同一个字符串对象: + +![][2] + +但是,之所以可以这么做,主要是因为字符串的不变性。试想一下,如果字符串是可变的,我们一旦修改了s的内容,那必然导致s2的内容也被动的改变了,这显然不是我们想看到的。 + +#### 安全性 + +字符串在Java应用程序中广泛用于存储敏感信息,如用户名、密码、连接url、网络连接等。JVM类加载器在加载类的时也广泛地使用它。 + +因此,保护String类对于提升整个应用程序的安全性至关重要。 + +当我们在程序中传递一个字符串的时候,如果这个字符串的内容是不可变的,那么我们就可以相信这个字符串中的内容。 + +但是,如果是可变的,那么这个字符串内容就可能随时都被修改。那么这个字符串内容就完全可信了。这样整个系统就没有安全性可言了。 + +#### 线程安全 + +不可变会自动使字符串成为线程安全的,因为当从多个线程访问它们时,它们不会被更改。 + +因此,一般来说,不可变对象可以在同时运行的多个线程之间共享。它们也是线程安全的,因为如果线程更改了值,那么将在字符串池中创建一个新的字符串,而不是修改相同的值。因此,字符串对于多线程来说是安全的。 + +#### hashcode缓存 + +由于字符串对象被广泛地用作数据结构,它们也被广泛地用于哈希实现,如HashMap、HashTable、HashSet等。在对这些散列实现进行操作时,经常调用hashCode()方法。 + +不可变性保证了字符串的值不会改变。因此,hashCode()方法在String类中被重写,以方便缓存,这样在第一次hashCode()调用期间计算和缓存散列,并从那时起返回相同的值。 + +在String类中,有以下代码: + +``` +private int hash;//this is used to cache hash code. +``` + + +#### 性能 + +前面提到了的字符串池、hashcode缓存等,都是提升性能的提现。 + +因为字符串不可变,所以可以用字符串池缓存,可以大大节省堆内存。而且还可以提前对hashcode进行缓存,更加高效 + +由于字符串是应用最广泛的数据结构,提高字符串的性能对提高整个应用程序的总体性能有相当大的影响。 + +### 总结 + +通过本文,我们可以得出这样的结论:字符串是不可变的,因此它们的引用可以被视为普通变量,可以在方法之间和线程之间传递它们,而不必担心它所指向的实际字符串对象是否会改变。 + +我们还了解了促使Java语言设计人员将该类设置为不可变类的其他原因。主要考虑的是缓存、安全性、线程安全和性能等方面 + + [1]: https://www.hollischuang.com/wp-content/uploads/2021/03/16163108328434.jpg + [2]: https://www.hollischuang.com/wp-content/uploads/2021/03/16163114985563.jpg \ No newline at end of file diff --git a/docs/basics/java-basic/inheritance-composition.md b/docs/basics/java-basic/inheritance-composition.md deleted file mode 100644 index 5eff0d32..00000000 --- a/docs/basics/java-basic/inheritance-composition.md +++ /dev/null @@ -1,67 +0,0 @@ -Java是一个面向对象的语言。每一个学习过Java的人都知道,封装、继承、多态是面向对象的三个特征。每个人在刚刚学习继承的时候都会或多或少的有这样一个印象:继承可以帮助我实现类的复用。所以,很多开发人员在需要复用一些代码的时候会很自然的使用类的继承的方式,因为书上就是这么写的(老师就是这么教的)。但是,其实这样做是不对的。长期大量的使用继承会给代码带来很高的维护成本。 - -本文将介绍组合和继承的概念及区别,并从多方面分析在写代码时如何进行选择。 - -## 面向对象的复用技术 - -前面提到复用,这里就简单介绍一下面向对象的复用技术。 - -复用性是面向对象技术带来的很棒的潜在好处之一。如果运用的好的话可以帮助我们节省很多开发时间,提升开发效率。但是,如果被滥用那么就可能产生很多难以维护的代码。 - -作为一门面向对象开发的语言,代码复用是Java引人注意的功能之一。Java代码的复用有继承,组合以及代理三种具体的表现形式。本文将重点介绍继承复用和组合复用。 - -## 继承 - -继承(Inheritance)是一种联结类与类的层次模型。指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系;继承是一种[`is-a`][1]关系。(图片来自网络,侵删。) - -![Inheritance][2] - -## 组合 - -组合(Composition)体现的是整体与部分、拥有的关系,即[`has-a`][3]的关系。 - -![Composition][4] - -## 组合与继承的区别和联系 - -> 在`继承`结构中,父类的内部细节对于子类是可见的。所以我们通常也可以说通过继承的代码复用是一种`白盒式代码复用`。(如果基类的实现发生改变,那么派生类的实现也将随之改变。这样就导致了子类行为的不可预知性;) -> -> `组合`是通过对现有的对象进行拼装(组合)产生新的、更复杂的功能。因为在对象之间,各自的内部细节是不可见的,所以我们也说这种方式的代码复用是`黑盒式代码复用`。(因为组合中一般都定义一个类型,所以在编译期根本不知道具体会调用哪个实现类的方法) -> -> `继承`,在写代码的时候就要指名具体继承哪个类,所以,在`编译期`就确定了关系。(从基类继承来的实现是无法在运行期动态改变的,因此降低了应用的灵活性。) -> -> `组合`,在写代码的时候可以采用面向接口编程。所以,类的组合关系一般在`运行期`确定。 - -## 优缺点对比 - -| 组 合 关 系 | 继 承 关 系 | -| -------------------------------- | -------------------------------------- | -| 优点:不破坏封装,整体类与局部类之间松耦合,彼此相对独立 | 缺点:破坏封装,子类与父类之间紧密耦合,子类依赖于父类的实现,子类缺乏独立性 | -| 优点:具有较好的可扩展性 | 缺点:支持扩展,但是往往以增加系统结构的复杂度为代价 | -| 优点:支持动态组合。在运行时,整体对象可以选择不同类型的局部对象 | 缺点:不支持动态继承。在运行时,子类无法选择不同的父类 | -| 优点:整体类可以对局部类进行包装,封装局部类的接口,提供新的接口 | 缺点:子类不能改变父类的接口 | -| 缺点:整体类不能自动获得和局部类同样的接口 | 优点:子类能自动继承父类的接口 | -| 缺点:创建整体类的对象时,需要创建所有局部类的对象 | 优点:创建子类的对象时,无须创建父类的对象 | - -## 如何选择 - -相信很多人都知道面向对象中有一个比较重要的原则『多用组合、少用继承』或者说『组合优于继承』。从前面的介绍已经优缺点对比中也可以看出,组合确实比继承更加灵活,也更有助于代码维护。 - -所以, - -> **`建议在同样可行的情况下,优先使用组合而不是继承。`** -> -> **`因为组合更安全,更简单,更灵活,更高效。`** - -注意,并不是说继承就一点用都没有了,前面说的是【在同样可行的情况下】。有一些场景还是需要使用继承的,或者是更适合使用继承。 - -> 继承要慎用,其使用场合仅限于你确信使用该技术有效的情况。一个判断方法是,问一问自己是否需要从新类向基类进行向上转型。如果是必须的,则继承是必要的。反之则应该好好考虑是否需要继承。《[Java编程思想][5]》 -> -> 只有当子类真正是超类的子类型时,才适合用继承。换句话说,对于两个类A和B,只有当两者之间确实存在[`is-a`][1]关系的时候,类B才应该继承类A。《[Effective Java][6]》 - - [1]: https://zh.wikipedia.org/wiki/Is-a - [2]: http://www.hollischuang.com/wp-content/uploads/2016/03/Generalization.jpg - [3]: https://en.wikipedia.org/wiki/Has-a - [4]: http://www.hollischuang.com/wp-content/uploads/2016/03/Composition.jpg - [5]: http://s.click.taobao.com/t?e=m%3D2%26s%3DHzJzud6zOdocQipKwQzePOeEDrYVVa64K7Vc7tFgwiHjf2vlNIV67vo5P8BMUBgoEC56fBbgyn5pS4hLH%2FP02ckKYNRBWOBBey11vvWwHXSniyi5vWXIZhtlrJbLMDAQihpQCXu2JnPFYKQlNeOGCsYMXU3NNCg%2F&pvid=10_125.119.86.125_222_1458652212179 - [6]: http://s.click.taobao.com/t?e=m%3D2%26s%3DwIPn8%2BNPqLwcQipKwQzePOeEDrYVVa64K7Vc7tFgwiHjf2vlNIV67vo5P8BMUBgoUOZr0mLjusdpS4hLH%2FP02ckKYNRBWOBBey11vvWwHXSniyi5vWXIZvgXwmdyquYbNLnO%2BjzYQLqKnzbV%2FMLqnMYMXU3NNCg%2F&pvid=10_125.119.86.125_345_1458652241780 diff --git a/docs/basics/java-basic/replace-in-string.md b/docs/basics/java-basic/replace-in-string.md index 3b66cfaf..9c9b31cc 100644 --- a/docs/basics/java-basic/replace-in-string.md +++ b/docs/basics/java-basic/replace-in-string.md @@ -24,5 +24,5 @@ replaceAll和replaceFirst的区别主要是替换的内容不同,replaceAll是 //使用replaceFirst将第一个数字替换成H System.out.println(string.replaceFirst("\\d","H"));//abcH23adb23456aa - //使用replaceFirst将所有数字替换成H + //使用replaceAll将所有数字替换成H System.out.println(string.replaceAll("\\d","H"));//abcHHHadbHHHHHaa \ No newline at end of file diff --git a/docs/basics/java-basic/scope.md b/docs/basics/java-basic/scope.md deleted file mode 100644 index fb44d530..00000000 --- a/docs/basics/java-basic/scope.md +++ /dev/null @@ -1,7 +0,0 @@ -### 对于成员变量和方法的作用域,public,protected,private以及不写之间的区别。 - - -- public :表明该成员变量或者方法是对所有类或者对象都是可见的,所有类或者对象都可以直接访问 -- private:表明该成员变量或者方法是私有的,只有当前类对其具有访问权限,除此之外其他类或者对象都没有访问权限.子类也没有访问权限. -- protected:表明成员变量或者方法对类自身,与同在一个包中的其他类可见,其他包下的类不可访问,除非是他的子类 -- default:表明该成员变量或者方法只有自己和其位于同一个包的内可见,其他包内的类不能访问,即便是它的子类 \ No newline at end of file diff --git a/docs/basics/java-basic/string-append.md b/docs/basics/java-basic/string-append.md index fea68cef..eae5eb66 100644 --- a/docs/basics/java-basic/string-append.md +++ b/docs/basics/java-basic/string-append.md @@ -1,3 +1,5 @@ +Java中,想要拼接字符串,最简单的方式就是通过"+"连接两个字符串。 + 有人把Java中使用+拼接字符串的功能理解为运算符重载。其实并不是,Java是不支持运算符重载的。这其实只是Java提供的一个语法糖。 >运算符重载:在计算机程序设计中,运算符重载(英语:operator overloading)是多态的一种。运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。 diff --git a/docs/basics/java-basic/substring.md b/docs/basics/java-basic/substring.md index a017b164..101f97d2 100644 --- a/docs/basics/java-basic/substring.md +++ b/docs/basics/java-basic/substring.md @@ -1,4 +1,6 @@ -String是Java中一个比较基础的类,每一个开发人员都会经常接触到。而且,String也是面试中经常会考的知识点。String有很多方法,有些方法比较常用,有些方法不太常用。今天要介绍的substring就是一个比较常用的方法,而且围绕substring也有很多面试题。 +String是Java中一个比较基础的类,每一个开发人员都会经常接触到。而且,String也是面试中经常会考的知识点。 + +String有很多方法,有些方法比较常用,有些方法不太常用。今天要介绍的substring就是一个比较常用的方法,而且围绕substring也有很多面试题。 `substring(int beginIndex, int endIndex)`方法在不同版本的JDK中的实现是不同的。了解他们的区别可以帮助你更好的使用他。为简单起见,后文中用`substring()`代表`substring(int beginIndex, int endIndex)`方法。 diff --git a/docs/basics/java-basic/switch-string.md b/docs/basics/java-basic/switch-string.md index deac11de..067e7ad4 100644 --- a/docs/basics/java-basic/switch-string.md +++ b/docs/basics/java-basic/switch-string.md @@ -1,7 +1,5 @@ Java 7中,switch的参数可以是String类型了,这对我们来说是一个很方便的改进。到目前为止switch支持这样几种数据类型:`byte` `short` `int` `char` `String` 。但是,作为一个程序员我们不仅要知道他有多么好用,还要知道它是如何实现的,switch对整型的支持是怎么实现的呢?对字符型是怎么实现的呢?String类型呢?有一点Java开发经验的人这个时候都会猜测switch对String的支持是使用equals()方法和hashcode()方法。那么到底是不是这两个方法呢?接下来我们就看一下,switch到底是如何实现的。 - - ### 一、switch对整型支持的实现 下面是一段很简单的Java代码,定义一个int型变量a,然后使用switch语句进行判断。执行这段代码输出内容为5,那么我们将下面这段代码反编译,看看他到底是怎么实现的。 diff --git a/docs/basics/java-basic/variable.md b/docs/basics/java-basic/variable.md deleted file mode 100644 index 8e715c7c..00000000 --- a/docs/basics/java-basic/variable.md +++ /dev/null @@ -1,29 +0,0 @@ -### 类变量、成员变量和局部变量 - -Java中共有三种变量,分别是类变量、成员变量和局部变量。他们分别存放在JVM的方法区、堆内存和栈内存中。 -```java - /** - * @author Hollis - */ - public class Variables { - - /** - * 类变量 - */ - private static int a; - - /** - * 成员变量 - */ - private int b; - - /** - * 局部变量 - * @param c - */ - public void test(int c){ - int d; - } - } -``` -上面定义的三个变量中,变量a就是类变量,变量b就是成员变量,而变量c和d是局部变量。 diff --git a/docs/basics/object-oriented/characteristics.md b/docs/basics/object-oriented/characteristics.md index 7b1e4a53..0ac4f280 100644 --- a/docs/basics/object-oriented/characteristics.md +++ b/docs/basics/object-oriented/characteristics.md @@ -1,4 +1,4 @@ - +我们说面向对象的开发范式,其实是对现实世界的理解和抽象的方法,那么,具体如何将现实世界抽象成代码呢?这就需要运用到面向对象的三大特性,分别是封装性、继承性和多态性。 ### 封装(Encapsulation) @@ -8,7 +8,7 @@ #### 封装举例 -如我们想要定义一个矩形,先定义一个Rectangle类,并其中通过封装的手段放入一些必备数据 +如我们想要定义一个矩形,先定义一个Rectangle类,并其中通过封装的手段放入一些必备数据。 /** * 矩形 @@ -42,6 +42,8 @@ return this.length * this.width; } } + +我们通过封装的方式,给"矩形"定义了"长度"和"宽度",这就完成了对现实世界中的"矩形"的抽象的第一步。 ### 继承(Inheritance) @@ -69,6 +71,7 @@ } } +现实世界中,"正方形"是"矩形"的特例,或者说正方形是通过矩形派生出来的,这种派生关系,在面向对象中可以用继承来表达。 ### 多态(Polymorphism) @@ -78,4 +81,10 @@ 最常见的多态就是将子类传入父类参数中,运行时调用父类方法时通过传入的子类决定具体的内部结构或行为。 -关于多态的例子,我们后面的章节中还会深入展开。 \ No newline at end of file +关于多态的例子,我们第二章中深入开展介绍。 + +在介绍了面向对象的封装、继承、多态的三个基本特征之后,我们基本掌握了对现实世界抽象的基本方法。 + +莎士比亚说:"一千个读者眼里有一千个​哈姆雷特",说到对现实世界的抽象,虽然方法相同,但是运用同样的方法,最终得到的结果可能千差万别,那么如何评价这个抽象的结果的好坏呢? + +这就要提到面喜爱那个对象的五大基本原则了,有了五大原则,我们参考他们来评价一个抽象的好坏。 \ No newline at end of file diff --git a/docs/basics/object-oriented/constructor.md b/docs/basics/object-oriented/constructor.md index 42f9dd74..894a6c26 100644 --- a/docs/basics/object-oriented/constructor.md +++ b/docs/basics/object-oriented/constructor.md @@ -1,4 +1,4 @@ -构造函数,是一种特殊的方法。 主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。 +构造函数,是一种特殊的方法。主要用来在创建对象时初始化对象,即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。 /** * 矩形 @@ -19,8 +19,6 @@ } } - - 特别的一个类可以有多个构造函数,可根据其参数个数的不同或参数类型的不同来区分它们即构造函数的重载。 diff --git a/docs/basics/object-oriented/extends-implement.md b/docs/basics/object-oriented/extends-implement.md index 536c5901..ca79513b 100644 --- a/docs/basics/object-oriented/extends-implement.md +++ b/docs/basics/object-oriented/extends-implement.md @@ -1,20 +1,19 @@ -前面的章节我们提到过面向对象有三个特征:封装、继承、多态。 +前面的章节我们提到过面向对象有三个特征:封装、继承、多态。前面我们分别介绍过了这三个特性。 -继承可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。这种派生方式提现了*传递性*,在Java中,除了继承,还有一种提现传递性的方式叫做实现。 +我们知道,继承可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。这种派生方式体现了*传递性*。 + +在Java中,除了继承,还有一种体现传递性的方式叫做实现。那么,这两者方式有什么区别呢? 继承和实现两者的明确定义和区别如下: ->继承(Inheritance):如果多个类的某个部分的功能相同,那么可以抽象出一个类出来,把他们的相同部分都放到父类里,让他们都继承这个类。 -> ->实现(Implement):如果多个类处理的目标是一样的,但是处理的方法方式不同,那么就定义一个接口,也就是一个标准,让他们的实现这个接口,各自实现自己具体的处理方法来处理那个目标 +继承(Inheritance):如果多个类的某个部分的功能相同,那么可以抽象出一个类出来,把他们的相同部分都放到父类里,让他们都继承这个类。 + +实现(Implement):如果多个类处理的目标是一样的,但是处理的方法方式不同,那么就定义一个接口,也就是一个标准,让他们的实现这个接口,各自实现自己具体的处理方法来处理那个目标 继承指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力。所以,继承的根本原因是因为要*复用*,而实现的根本原因是需要定义一个*标准*。 在Java中,继承使用`extends`关键字实现,而实现通过`implements`关键字。 - -特别需要注意的是,Java中支持一个类同时实现多个接口,但是不支持同时继承多个类。但是这个问题在Java 8之后也不绝对了。 - >简单点说,就是同样是一台汽车,既可以是电动车,也可以是汽油车,也可以是油电混合的,只要实现不同的标准就行了,但是一台车只能属于一个品牌,一个厂商。 ``` @@ -26,4 +25,6 @@ 以上,我们定义了一辆汽车,他实现了电动车和汽油车两个标准,但是他属于奔驰这个品牌。像上面这样定义,我们可以最大程度的遵守标准,并且复用奔驰车所有已有的一些功能组件。 -另外,在接口中只能定义全局常量(static final)和无实现的方法(Java 8以后可以有defult方法);而在继承中可以定义属性方法,变量,常量等。 +另外,在接口中只能定义全局常量(static final)和无实现的方法(Java 8以后可以有default方法);而在继承中可以定义属性方法,变量,常量等。 + +*特别需要注意的是,Java中支持一个类同时实现多个接口,但是不支持同时继承多个类。* 但是这个问题在Java 8之后也不绝对了。关于多继承的问题,我们下一个章节中介绍。 \ No newline at end of file diff --git a/docs/basics/object-oriented/inheritance-composition.md b/docs/basics/object-oriented/inheritance-composition.md index e2869f3a..14b70340 100644 --- a/docs/basics/object-oriented/inheritance-composition.md +++ b/docs/basics/object-oriented/inheritance-composition.md @@ -26,7 +26,7 @@ 组合(Composition)体现的是整体与部分、拥有的关系,即[`has-a`][3]的关系。 -> is-a:表示"有一个"的关系,如狗有一个尾巴 +> has-a:表示"有一个"的关系,如狗有一个尾巴 ![Composition][4] diff --git a/docs/basics/object-oriented/java-pass-by.md b/docs/basics/object-oriented/java-pass-by.md index 895f2a2f..c05138bc 100644 --- a/docs/basics/object-oriented/java-pass-by.md +++ b/docs/basics/object-oriented/java-pass-by.md @@ -1,57 +1,98 @@ +关于Java中方法间的参数传递到底是怎样的、为什么很多人说Java只有值传递等问题,一直困惑着很多人,甚至我在面试的时候问过很多有丰富经验的开发者,他们也很难解释的很清楚。 + +我很久也写过一篇文章,我当时认为我把这件事说清楚了,但是,最近在整理这部分知识点的时候,我发现我当时理解的还不够透彻,于是我想着通过Google看看其他人怎么理解的,但是遗憾的是没有找到很好的资料可以说的很清楚。 + +于是,我决定尝试着把这个话题总结一下,重新理解一下这个问题。 + +### 辟谣时间 + +关于这个问题,在StackOverflow上也引发过广泛的讨论,看来很多程序员对于这个问题的理解都不尽相同,甚至很多人理解的是错误的。还有的人可能知道Java中的参数传递是值传递,但是说不出来为什么。 + +在开始深入讲解之前,有必要纠正一下大家以前的那些错误看法了。如果你有以下想法,那么你有必要好好阅读本文。 + +> 错误理解一:值传递和引用传递,区分的条件是传递的内容,如果是个值,就是值传递。如果是个引用,就是引用传递。 +> +> 错误理解二:Java是引用传递。 +> +> 错误理解三:传递的参数如果是普通类型,那就是值传递,如果是对象,那就是引用传递。 ### 实参与形参 我们都知道,在Java中定义方法的时候是可以定义参数的。比如Java中的main方法,`public static void main(String[] args)`,这里面的args就是参数。参数在程序语言中分为形式参数和实际参数。 > 形式参数:是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传入的参数。 -> +> > 实际参数:在调用有参函数时,主调函数和被调函数之间有数据传递关系。在主调函数中调用一个函数时,函数名后面括号中的参数称为“实际参数”。 简单举个例子: -``` -public static void main(String[] args) { - ParamTest pt = new ParamTest(); - pt.sout("Hollis");//实际参数为 Hollis -} - -public void sout(String name) { //形式参数为 name - System.out.println(name); -} -``` + public static void main(String[] args) { + ParamTest pt = new ParamTest(); + pt.sout("Hollis");//实际参数为 Hollis + } + + public void sout(String name) { //形式参数为 name + System.out.println(name); + } + 实际参数是调用有参方法的时候真正传递的内容,而形式参数是用于接收实参内容的参数。 -### 值传递与引用传递 +### 求值策略 + +我们说当进行方法调用的时候,需要把实际参数传递给形式参数,那么传递的过程中到底传递的是什么东西呢? + +这其实是程序设计中**求值策略(Evaluation strategies)**的概念。 + +在计算机科学中,求值策略是确定编程语言中表达式的求值的一组(通常确定性的)规则。求值策略定义何时和以何种顺序求值给函数的实际参数、什么时候把它们代换入函数、和代换以何种形式发生。 + +求值策略分为两大基本类,基于如何处理给函数的实际参数,分位严格的和非严格的。 + +#### 严格求值 + +在“严格求值”中,函数调用过程中,给函数的实际参数总是在应用这个函数之前求值。多数现存编程语言对函数都使用严格求值。所以,我们本文只关注严格求值。 + +在严格求值中有几个关键的求值策略是我们比较关心的,那就是**传值调用**(Call by value)、**传引用调用**(Call by reference)以及**传共享对象调用**(Call by sharing)。 + +* 传值调用(值传递) + * 在传值调用中,实际参数先被求值,然后其值通过复制,被传递给被调函数的形式参数。因为形式参数拿到的只是一个"局部拷贝",所以如果在被调函数中改变了形式参数的值,并不会改变实际参数的值。 +* 传引用调用(应用传递) + * 在传引用调用中,传递给函数的是它的实际参数的隐式引用而不是实参的拷贝。因为传递的是引用,所以,如果在被调函数中改变了形式参数的值,改变对于调用者来说是可见的。 +* 传共享对象调用(共享对象传递) + * 传共享对象调用中,先获取到实际参数的地址,然后将其复制,并把该地址的拷贝传递给被调函数的形式参数。因为参数的地址都指向同一个对象,所以我们称也之为"传共享对象",所以,如果在被调函数中改变了形式参数的值,调用者是可以看到这种变化的。 + +不知道大家有没有发现,其实传共享对象调用和传值调用的过程几乎是一样的,都是进行"求值"、"拷贝"、"传递"。你品,你细品。 + +![][1] + +但是,传共享对象调用和内传引用调用的结果又是一样的,都是在被调函数中如果改变参数的内容,那么这种改变也会对调用者有影响。你再品,你再细品。 + +那么,共享对象传递和值传递以及引用传递之间到底有很么关系呢? -上面提到了,当我们调用一个有参函数的时候,会把实际参数传递给形式参数。但是,在程序语言中,这个传递过程中传递的两种情况,即值传递和引用传递。我们来看下程序语言中是如何定义和区分值传递和引用传递的。 +对于这个问题,我们应该关注过程,而不是结果,**因为传共享对象调用的过程和传值调用的过程是一样的,而且都有一步关键的操作,那就是"复制",所以,通常我们认为传共享对象调用是传值调用的特例** -> 值传递(pass by value)是指在调用函数时将实际参数`复制`一份传递到函数中,这样在函数中如果对`参数`进行修改,将不会影响到实际参数。 -> -> 引用传递(pass by reference)是指在调用函数时将实际参数的地址`直接`传递到函数中,那么在函数中对`参数`所进行的修改,将影响到实际参数。 +我们先把传共享对象调用放在一边,我们再来回顾下传值调用和传引用调用的主要区别: -那么,我来给大家总结一下,值传递和引用传递之前的区别的重点是什么: +**传值调用是指在调用函数时将实际参数`复制`一份传递到函数中,传引用调用是指在调用函数时将实际参数的引用`直接`传递到函数中。** -pass +![pass-by-reference-vs-pass-by-value-animation][2] -这里我们来举一个形象的例子。再来深入理解一下值传递和引用传递: +所以,两者的最主要区别就是是直接传递的,还是传递的是一个副本。 -你有一把钥匙,当你的朋友想要去你家的时候,如果你`直接`把你的钥匙给他了,这就是引用传递。这种情况下,如果他对这把钥匙做了什么事情,比如他在钥匙上刻下了自己名字,那么这把钥匙还给你的时候,你自己的钥匙上也会多出他刻的名字。 +这里我们来举一个形象的例子。再来深入理解一下传值调用和传引用调用: -你有一把钥匙,当你的朋友想要去你家的时候,你`复刻`了一把新钥匙给他,自己的还在自己手里,这就是值传递。这种情况下,他对这把钥匙做什么都不会影响你手里的这把钥匙。 +你有一把钥匙,当你的朋友想要去你家的时候,如果你`直接`把你的钥匙给他了,这就是引用传递。 +这种情况下,如果他对这把钥匙做了什么事情,比如他在钥匙上刻下了自己名字,那么这把钥匙还给你的时候,你自己的钥匙上也会多出他刻的名字。 -### 参考资料 +你有一把钥匙,当你的朋友想要去你家的时候,你`复刻`了一把新钥匙给他,自己的还在自己手里,这就是值传递。 -[Evaluation strategy][7] +这种情况下,他对这把钥匙做什么都不会影响你手里的这把钥匙。 -[关于值传递和引用传递][8] +前面我们介绍过了传值调用、传引用调用以及传值调用的特例传共享对象调用,那么,Java中是采用的哪种求值策略呢? -[按值传递、按引用传递、按共享传递][9] +下一篇我们深入分析。 -[Is Java “pass-by-reference” or “pass-by-value”?][2] -[2]: https://stackoverflow.com/questions/40480/is-java-pass-by-reference-or-pass-by-value -[7]: https://en.wikipedia.org/wiki/Evaluation_strategy -[8]: http://chenwenbo.github.io/2016/05/11/%E5%85%B3%E4%BA%8E%E5%80%BC%E4%BC%A0%E9%80%92%E5%92%8C%E5%BC%95%E7%94%A8%E4%BC%A0%E9%80%92/ -[9]: http://menzhongxin.com/2017/02/07/%E6%8C%89%E5%80%BC%E4%BC%A0%E9%80%92-%E6%8C%89%E5%BC%95%E7%94%A8%E4%BC%A0%E9%80%92%E5%92%8C%E6%8C%89%E5%85%B1%E4%BA%AB%E4%BC%A0%E9%80%92/ + [1]: http://www.hollischuang.com/wp-content/uploads/2020/04/15865905252659.jpg + [2]: http://www.hollischuang.com/wp-content/uploads/2020/04/pass-by-reference-vs-pass-by-value-animation.gif \ No newline at end of file diff --git a/docs/basics/object-oriented/multiple-inheritance.md b/docs/basics/object-oriented/multiple-inheritance.md index b6bcb235..308636b5 100644 --- a/docs/basics/object-oriented/multiple-inheritance.md +++ b/docs/basics/object-oriented/multiple-inheritance.md @@ -41,7 +41,7 @@ 虽然我们还是没办法使用extends同时继承多个类,但是因为有了默认函数,我们有可能通过implements从多个接口中继承到多个默认函数,那么,又如何解决这种情况带来的菱形继承问题呢? -这个问题,我们在后面的Java 8部分单独介绍。 +这个问题,我们在后面第20.4章节中单独介绍。 [1]: https://www.hollischuang.com/wp-content/uploads/2021/02/16145019571199.jpg \ No newline at end of file diff --git a/docs/basics/object-oriented/object-oriented-vs-procedure-oriented.md b/docs/basics/object-oriented/object-oriented-vs-procedure-oriented.md index dd8232a1..cb988ce5 100644 --- a/docs/basics/object-oriented/object-oriented-vs-procedure-oriented.md +++ b/docs/basics/object-oriented/object-oriented-vs-procedure-oriented.md @@ -1,42 +1,40 @@ -面向对象和面向过程是两种软件开发方法,或者说是两种不同的开发范式。 +相信很多Java开发者,在最初接触Java的时候就听说过,Java是一种面向对象的开发语言,那么什么是面向对象呢? +首先,所谓面向对象,其实是指软件工程中的一类编程风格,很多人称呼他们为开发范式、编程泛型(Programming Paradigm)。面向对象是众多开发范式中的一种。除了面向对象以外,还有面向过程、指令式编程、函数式编程等。 -### 什么是面向过程? - -“面向过程”(Procedure Oriented)是一种以过程为中心的编程思想,是一种自顶而下的编程模式。 - +虽然这几年函数式编程越来越被人们所熟知,但是,在所有的开发范式中,我们接触最多的主要还是面向过程和面向对象两种。 -最典型的面向过程的编程语言就是C语言。 +那么,在本书的第一章的第一篇,我们来简单介绍下,什么是面向过程和面向对象。 -#### 概述 -把问题分解成一个一个步骤,每个步骤用函数实现,依次调用即可。 +### 什么是面向过程? -就是说,在进行面向过程编程的时候,不需要考虑那么多,上来先定义一个函数,然后使用各种诸如if-else、for-each等方式进行代码执行。 +面向过程(Procedure Oriented)是一种以过程为中心的编程思想,是一种自顶而下的编程模式。最典型的面向过程的编程语言就是C语言。 -最典型的用法就是实现一个简单的算法,比如实现冒泡排序。 +简单来说,面向过程的开发范式中,程序员需要把问题分解成一个一个步骤,每个步骤用函数实现,依次调用即可。 +就是说,在进行面向过程编程的时候,不需要考虑那么多,上来先定义一个函数,然后使用各种诸如if-else、for-each等方式进行代码执行。最典型的用法就是实现一个简单的算法,比如实现冒泡排序。 -### 什么是面向对象? +面向过程进行的软件开发,其代码都是流程化的,很明确的可以看出第一步做什么、第二步做什么。这种方式的代码执行起来效率很高。 -面向对象程序设计的雏形,早在出现在1960年的Simula语言中,当时的程序设计领域正面临着一种危机:在软硬件环境逐渐复杂的情况下,软件如何得到良好的维护? +但是,面向过程同时存在着代码重用性低,扩展能力差,后期维护难度比较大等问题。 -面向对象程序设计在某种程度上通过强调可重复性解决了这一问题。 -目前较为流行的面向对象语言主要有Java、C#、C++、Python、Ruby、PHP等 +### 什么是面向对象? -面向对象是一种将事务高度抽象化的编程模式 +面向对象(Object Oriented)的雏形,最早在出现在1960年的Simula语言中,当时的程序设计领域正面临着一种危机:在软硬件环境逐渐复杂的情况下,软件如何得到良好的维护? -#### 概述: +面向对象程序设计在某种程度上通过强调可重复性解决了这一问题。目前较为流行的面向对象语言主要有Java、C#、C++、Python、Ruby、PHP等。 -将问题分解成一个一个步骤,对每个步骤进行相应的抽象,形成对象,通过不同对象之间的调用,组合解决问题。 +简单来说,面向对象的开发范式中,程序员将问题分解成一个一个步骤,对每个步骤进行相应的抽象,形成对象,通过不同对象之间的调用,组合解决问题。 -就是说,在进行面向对象进行编程的时候,要把属性、行为等封装成对象,然后基于这些对象及对象的能力进行业务逻辑的实现。 +就是说,在进行面向对象进行编程的时候,要把属性、行为等封装成对象,然后基于这些对象及对象的能力进行业务逻辑的实现。比如:想要造一辆车,上来要先把车的各种属性定义出来,然后抽象成一个Car类。 -比如:想要造一辆车,上来要先把车的各种属性定义出来,然后抽象成一个Car类。 +面向对象的编程方法之所以更加受欢迎,是因为他更加符合人类的思维方式。这种方式编写出来的代码扩展性、可维护性都很高。 +与其实面向对象是一种开发范式,倒不如说面向对象是一种对现实世界的理解和抽象的方法。通过对现实世界的理解和抽象,在运用封装、继承、多态等方法,通过抽象出对象的方式进行软件开发。 -面向对象具有三大基本特征和五大基本原则,这一点在后面的章节中展开介绍。 +什么是封装、继承、多态?具体如何运营面向对象的方式编写代码呢?接下来我们介绍下面向对象具有三大基本特征和五大基本原则。 diff --git a/docs/basics/object-oriented/overloading-vs-overriding.md b/docs/basics/object-oriented/overloading-vs-overriding.md index 2e08cd24..952316d6 100644 --- a/docs/basics/object-oriented/overloading-vs-overriding.md +++ b/docs/basics/object-oriented/overloading-vs-overriding.md @@ -1,28 +1,39 @@ -![overloading-vs-overriding][1] - -重载(Overloading)和重写(Overriding)是Java中两个比较重要的概念。但是对于新手来说也比较容易混淆。本文通过两个简单的例子说明了他们之间的区别。 +重载(Overloading)和重写(Overriding)是Java中两个比较重要的概念。但是对于新手来说也比较容易混淆,本文就举两个实际的例子,来说明下到底是什么是重写和重载。 ## 定义 -### 重载 +首先我们分别来看一下重载和重写的定义: + +重载:指的是在同一个类中,多个函数或者方法有同样的名称,但是参数列表不相同的情形,这样的同名不同参数的函数或者方法之间,互相称之为重载函数或者方法。 -简单说,就是函数或者方法有同样的名称,但是参数列表不相同的情形,这样的同名不同参数的函数或者方法之间,互相称之为重载函数或者方法。 +重写:指的是在Java的子类与父类中有两个名称、参数列表都相同的方法的情况。由于他们具有相同的方法签名,所以子类中的新方法将覆盖父类中原有的方法。 + +## 重载的例子 -### 重写 + class Dog{ + public void bark(){ + System.out.println("woof "); + } + + //overloading method + public void bark(int num){ + for(int i=0; i 1、重载是一个编译期概念、重写是一个运行期间概念。 -> -> 2、重载遵循所谓“编译期绑定”,即在编译时根据参数变量的类型判断应该调用哪个方法。 -> -> 3、重写遵循所谓“运行期绑定”,即在运行的时候,根据引用变量所指向的实际对象的类型来调用方法 -> -> 4、因为在编译期已经确定调用哪个方法,所以重载并不是多态。而重写是多态。重载只是一种语言特性,是一种语法规则,与多态无关,与面向对象也无关。(注:严格来说,重载是编译时多态,即静态多态。但是,Java中提到的多态,在不特别说明的情况下都指动态多态) +1、被重载的方法必须改变参数列表; +2、被重载的方法可以改变返回类型; +3、被重载的方法可以改变访问修饰符; +4、被重载的方法可以声明新的或更广的检查异常; +5、方法能够在同一个类中或者在一个子类中被重载。 ## 重写的例子 @@ -56,58 +67,21 @@ bowl -上面的例子中,`dog`对象被定义为`Dog`类型。在编译期,编译器会检查Dog类中是否有可访问的`bark()`方法,只要其中包含`bark()`方法,那么就可以编译通过。在运行期,`Hound`对象被`new`出来,并赋值给`dog`变量,这时,JVM是明确的知道`dog`变量指向的其实是`Hound`对象的引用。所以,当`dog`调用`bark()`方法的时候,就会调用`Hound`类中定义的`bark()`方法。这就是所谓的动态多态性。 - -### 重写的条件 - -> 参数列表必须完全与被重写方法的相同; -> -> 返回类型必须完全与被重写方法的返回类型相同; -> -> 访问级别的限制性一定不能比被重写方法的强; -> -> 访问级别的限制性可以比被重写方法的弱; -> -> 重写方法一定不能抛出新的检查异常或比被重写的方法声明的检查异常更广泛的检查异常 -> -> 重写的方法能够抛出更少或更有限的异常(也就是说,被重写的方法声明了异常,但重写的方法可以什么也不声明) -> -> 不能重写被标示为final的方法; -> -> 如果不能继承一个方法,则不能重写这个方法。 - -## 重载的例子 - - class Dog{ - public void bark(){ - System.out.println("woof "); - } - - //overloading method - public void bark(int num){ - for(int i=0; i 被重载的方法必须改变参数列表; -> -> 被重载的方法可以改变返回类型; -> -> 被重载的方法可以改变访问修饰符; -> -> 被重载的方法可以声明新的或更广的检查异常; -> -> 方法能够在同一个类中或者在一个子类中被重载。 +在编译期,编译器会检查Dog类中是否有可访问的`bark()`方法,只要其中包含`bark()`方法,那么就可以编译通过。 -## 参考资料 +在运行期,`Hound`对象被`new`出来,并赋值给`dog`变量,这时,JVM是明确的知道`dog`变量指向的其实是`Hound`对象的引用。所以,当`dog`调用`bark()`方法的时候,就会调用`Hound`类中定义的`bark()`方法。这就是所谓的动态多态性。 -[Overriding vs. Overloading in Java][2] +方法重写的条件需要具备以下条件和要求: - [1]: http://www.hollischuang.com/wp-content/uploads/2016/03/overloading-vs-overriding.png - [2]: http://www.programcreek.com/2009/02/overriding-and-overloading-in-java-with-examples/ \ No newline at end of file +1、参数列表必须完全与被重写方法的相同; +2、返回类型必须完全与被重写方法的返回类型相同; +3、访问级别的限制性一定不能比被重写方法的强; +4、访问级别的限制性可以比被重写方法的弱; +5、重写方法一定不能抛出新的检查异常或比被重写的方法声明的检查异常更广泛的检查异常 +6、重写的方法能够抛出更少或更有限的异常(也就是说,被重写的方法声明了异常,但重写的方法可以什么也不声明) +7、不能重写被标示为final的方法; +8、如果不能继承一个方法,则不能重写这个方法。 \ No newline at end of file diff --git a/docs/basics/object-oriented/polymorphism.md b/docs/basics/object-oriented/polymorphism.md index 5289d367..8fa3608d 100644 --- a/docs/basics/object-oriented/polymorphism.md +++ b/docs/basics/object-oriented/polymorphism.md @@ -1,12 +1,42 @@ -### 什么是多态 +在第1.2章节中,我们介绍了面向对象的封装、继承和多态这三个基本特性,并且分别对封装和继承简单的举例做了说明。 -多态的概念呢比较简单,就是同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。 +这一章节中,我们针对上一章节遗留的多态性进行展开介绍。 -如果按照这个概念来定义的话,那么多态应该是一种运行期的状态。 -### 多态的必要条件 +## 什么是多态 -为了实现运行期的多态,或者说是动态绑定,需要满足三个条件: +我们先基于所有的编程语言介绍了什么是多态以及多态的分类。然后再重点介绍下Java中的多态。 + +多态(Polymorphism),指为不同数据类型的实体提供统一的接口,或使用一个单一的符号来表示多个不同的类型。一般情况下,可以把多态分成以下几类: + +* 特设多态:为个体的特定类型的任意集合定义一个共同接口。 +* 参数多态:指定一个或多个类型不靠名字而是靠可以标识任何类型的抽象符号。 +* 子类型:一个名字指称很多不同的类的实例,这些类有某个共同的超类。 + +### 特设多态 + +特设多态是程序设计语言的一种多态,多态函数有多个不同的实现,依赖于其实参而调用相应版本的函数。 + +上一节我们介绍过的函数重载是特设多态的一种,除此之外还有运算符重载也是特设多态的一种。 + + +### 参数多态 + +参数多态在程序设计语言与类型论中是指声明与定义函数、复合类型、变量时不指定其具体的类型,而把这部分类型作为参数使用,使得该定义对各种具体类型都适用。 + +参数多态其实也有很广泛的应用,比如Java中的泛型就是参数多态的一种。参数多态另外一个应用比较广泛的地方就是函数式编程。 + +### 子类型 + +在面向对象程序设计中,计算机程序运行时,相同的消息可能会送给多个不同的类别之对象,而系统可依据对象所属类别,引发对应类别的方法,而有不同的行为。 + +这种子类型多态其实就是Java中常见的多态,下面我们针对Java中的这种子类型多态展开介绍下。 + +## Java中的多态 + +Java中的多态的概念比较简单,就是同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。 + +Java中多态其实是一种运行期的状态。为了实现运行期的多态,或者说是动态绑定,需要满足三个条件: * 有类继承或者接口实现 * 子类要重写父类的方法 @@ -53,10 +83,18 @@ ### 静态多态 -上面我们说的多态,是一种运行期的概念。另外,还有一种说法,包括维基百科也说明,多态还分为动态多态和静态多态。 +上面我们说的多态,是一种运行期的概念。另外,还有一种说法,认为多态还分为动态多态和静态多态。 上面提到的那种动态绑定认为是动态多态,因为只有在运行期才能知道真正调用的是哪个类的方法。 很多人认为,还有一种静态多态,一般认为Java中的函数重载是一种静态多态,因为他需要在编译期决定具体调用哪个方法。 -但是,其实作者认为,多态应该是一种运行期特性,Java中的方法重写是多态的体现。虽然也有人提出重载是一种静态多态的想法,这个问题在StackOverflow等网站上有很多人讨论,但是并没有什么定论。我更加倾向于重载不是多态。 +结合2.1章节,我们介绍过的重载和重写的相关概念,我们再来总结下重载和重写这两个概念: + +1、重载是一个编译期概念、重写是一个运行期概念。 + +2、重载遵循所谓“编译期绑定”,即在编译时根据参数变量的类型判断应该调用哪个方法。 + +3、重写遵循所谓“运行期绑定”,即在运行的时候,根据引用变量所指向的实际对象的类型来调用方法。 + +4、Java中的方法重写是Java多态(子类型)的实现方式。而Java中的方法重写其实是特设多态的一种实现方式。 \ No newline at end of file diff --git a/docs/basics/object-oriented/principle.md b/docs/basics/object-oriented/principle.md index 43e92a59..2f93e606 100644 --- a/docs/basics/object-oriented/principle.md +++ b/docs/basics/object-oriented/principle.md @@ -1,40 +1,66 @@ +面向对象开发范式的最大的好处就是易用、易扩展、易维护,但是,什么样的代码是易用、易扩展、易维护的呢?如何衡量他们呢? - +罗伯特·C·马丁在21世纪早期提出了SOLID原则,这是五个原则的缩写的组合,这五个原则沿用至今。 ### 单一职责原则(Single-Responsibility Principle) -其核心思想为:一个类,最好只做一件事,只有一个引起它的变化。单一职责原则可以看做是低耦合、高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。职责过多,可能引起它变化的原因就越多,这将导致职责依赖,相互之间就产生影响,从而大大损伤其内聚性和耦合度。通常意义下的单一职责,就是指只有一种单一功能,不要为类实现过多的功能点,以保证实体只有一个引起它变化的原因。 +其核心思想为:一个类,最好只做一件事,只有一个引起它的变化。 + +单一职责原则可以看做是低耦合、高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。职责过多,可能引起它变化的原因就越多,这将导致职责依赖,相互之间就产生影响,从而大大损伤其内聚性和耦合度。通常意义下的单一职责,就是指只有一种单一功能,不要为类实现过多的功能点,以保证实体只有一个引起它变化的原因。 专注,是一个人优良的品质;同样的,单一也是一个类的优良设计。交杂不清的职责将使得代码看起来特别别扭牵一发而动全身,有失美感和必然导致丑陋的系统错误风险。 ### 开放封闭原则(Open-Closed principle) -其核心思想是:软件实体应该是可扩展的,而不可修改的。也就是,对扩展开放,对修改封闭的。开放封闭原则主要体现在两个方面1、对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。2、对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对其进行任何尝试的修改。 +其核心思想是:软件实体应该是可扩展的,而不可修改的。也就是,对扩展开放,对修改封闭的。 + +开放封闭原则主要体现在两个方面: + +1、对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。 + +2、对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对其进行任何尝试的修改。 + 实现开放封闭原则的核心思想就是对抽象编程,而不对具体编程,因为抽象相对稳定。让类依赖于固定的抽象,所以修改就是封闭的;而通过面向对象的继承和多态机制,又可以实现对抽象类的继承,通过覆写其方法来改变固有行为,实现新的拓展方法,所以就是开放的。 “需求总是变化”没有不变的软件,所以就需要用封闭开放原则来封闭变化满足需求,同时还能保持软件内部的封装体系稳定,不被需求的变化影响。 -### Liskov替换原则(Liskov-Substitution Principle) +### 里氏替换原则(Liskov-Substitution Principle) + +其核心思想是:子类必须能够替换其基类。这一思想体现为对继承机制的约束规范,只有子类能够替换基类时,才能保证系统在运行期内识别子类,这是保证继承复用的基础。 + +在父类和子类的具体行为中,必须严格把握继承层次中的关系和特征,将基类替换为子类,程序的行为不会发生任何变化。同时,这一约束反过来则是不成立的,子类可以替换基类,但是基类不一定能替换子类。 +里氏替换原则,主要着眼于对抽象和多态建立在继承的基础上,因此只有遵循了Liskov替换原则,才能保证继承复用是可靠地。实现的方法是面向接口编程:将公共部分抽象为基类接口或抽象类,通过Extract Abstract Class,在子类中通过覆写父类的方法实现新的方式支持同样的职责。 + +里氏替换原则是关于继承机制的设计原则,违反了Liskov替换原则就必然导致违反开放封闭原则。 -其核心思想是:子类必须能够替换其基类。这一思想体现为对继承机制的约束规范,只有子类能够替换基类时,才能保证系统在运行期内识别子类,这是保证继承复用的基础。在父类和子类的具体行为中,必须严格把握继承层次中的关系和特征,将基类替换为子类,程序的行为不会发生任何变化。同时,这一约束反过来则是不成立的,子类可以替换基类,但是基类不一定能替换子类。 -Liskov替换原则,主要着眼于对抽象和多态建立在继承的基础上,因此只有遵循了Liskov替换原则,才能保证继承复用是可靠地。实现的方法是面向接口编程:将公共部分抽象为基类接口或抽象类,通过Extract Abstract Class,在子类中通过覆写父类的方法实现新的方式支持同样的职责。 -Liskov替换原则是关于继承机制的设计原则,违反了Liskov替换原则就必然导致违反开放封闭原则。 -Liskov替换原则能够保证系统具有良好的拓展性,同时实现基于多态的抽象机制,能够减少代码冗余,避免运行期的类型判别。 +里氏替换原则能够保证系统具有良好的拓展性,同时实现基于多态的抽象机制,能够减少代码冗余,避免运行期的类型判别。 ### 依赖倒置原则(Dependecy-Inversion Principle) 其核心思想是:依赖于抽象。具体而言就是高层模块不依赖于底层模块,二者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象。 + 我们知道,依赖一定会存在于类与类、模块与模块之间。当两个模块之间存在紧密的耦合关系时,最好的方法就是分离接口和实现:在依赖之间定义一个抽象的接口使得高层模块调用接口,而底层模块实现接口的定义,以此来有效控制耦合关系,达到依赖于抽象的设计目标。 抽象的稳定性决定了系统的稳定性,因为抽象是不变的,依赖于抽象是面向对象设计的精髓,也是依赖倒置原则的核心。 + 依赖于抽象是一个通用的原则,而某些时候依赖于细节则是在所难免的,必须权衡在抽象和具体之间的取舍,方法不是一层不变的。依赖于抽象,就是对接口编程,不要对实现编程。 ### 接口隔离原则(Interface-Segregation Principle) 其核心思想是:使用多个小的专门的接口,而不要使用一个大的总接口。 + 具体而言,接口隔离原则体现在:接口应该是内聚的,应该避免“胖”接口。一个类对另外一个类的依赖应该建立在最小的接口上,不要强迫依赖不用的方法,这是一种接口污染。 + 接口有效地将细节和抽象隔离,体现了对抽象编程的一切好处,接口隔离强调接口的单一性。而胖接口存在明显的弊端,会导致实现的类型必须完全实现接口的所有方法、属性等;而某些时候,实现类型并非需要所有的接口定义,在设计上这是“浪费”,而且在实施上这会带来潜在的问题,对胖接口的修改将导致一连串的客户端程序需要修改,有时候这是一种灾难。在这种情况下,将胖接口分解为多个特点的定制化方法,使得客户端仅仅依赖于它们的实际调用的方法,从而解除了客户端不会依赖于它们不用的方法。 -分离的手段主要有以下两种:1、委托分离,通过增加一个新的类型来委托客户的请求,隔离客户和接口的直接依赖,但是会增加系统的开销。2、多重继承分离,通过接口多继承来实现客户的需求,这种方式是较好的。 +分离的手段主要有以下两种: + +1、委托分离,通过增加一个新的类型来委托客户的请求,隔离客户和接口的直接依赖,但是会增加系统的开销。 + +2、多重继承分离,通过接口多继承来实现客户的需求,这种方式是较好的。 + +以上就是5个基本的面向对象设计原则,它们就像面向对象程序设计中的金科玉律,遵守它们可以使我们的代码更加鲜活,易于复用,易于拓展,灵活优雅。 + +不同的设计模式对应不同的需求,而设计原则则代表永恒的灵魂,需要在实践中时时刻刻地遵守。就如ARTHUR J.RIEL在那边《OOD启示录》中所说的:“你并不必严格遵守这些原则,违背它们也不会被处以宗教刑罚。但你应当把这些原则看做警铃,若违背了其中的一条,那么警铃就会响起。” -以上就是5个基本的面向对象设计原则,它们就像面向对象程序设计中的金科玉律,遵守它们可以使我们的代码更加鲜活,易于复用,易于拓展,灵活优雅。不同的设计模式对应不同的需求,而设计原则则代表永恒的灵魂,需要在实践中时时刻刻地遵守。就如ARTHUR J.RIEL在那边《OOD启示录》中所说的:“你并不必严格遵守这些原则,违背它们也不会被处以宗教刑罚。但你应当把这些原则看做警铃,若违背了其中的一条,那么警铃就会响起。” +很多人刚开始可能对这些原则无法深刻的理解,但是没关系,随着自己开发经验的增长,就会慢慢的可以理解这些原则了。 \ No newline at end of file diff --git a/docs/basics/object-oriented/scope.md b/docs/basics/object-oriented/scope.md index 39c43aad..f03889bf 100644 --- a/docs/basics/object-oriented/scope.md +++ b/docs/basics/object-oriented/scope.md @@ -1,3 +1,6 @@ +我们通过封装的手段,将成员变量、方法等包装在一个类中,那么,被封装在类中的这些成员变量和方法,能不能被外部访问呢?能被谁访问呢? + +这种能不能被访问、能被谁访问的特性,Java是通过访问控制修饰符来实现的。Java中,可以使用访问控制符来保护对类、变量、方法和构造方法的访问,Java 支持 4 种不同的访问权限。 对于成员变量和方法的作用域,public,protected,private以及不写之间的区别: @@ -8,4 +11,5 @@ `protected` : 表明成员变量或者方法对类自身,与同在一个包中的其他类可见,其他包下的类不可访问,除非是他的子类 -`default` : 表明该成员变量或者方法只有自己和其位于同一个包的内可见,其他包内的类不能访问,即便是它的子类 \ No newline at end of file +`default` : 表明该成员变量或者方法只有自己和其位于同一个包的内可见,其他包内的类不能访问,即便是它的子类 + diff --git a/docs/basics/object-oriented/variable.md b/docs/basics/object-oriented/variable.md index cc3849c2..b2ba4b2a 100644 --- a/docs/basics/object-oriented/variable.md +++ b/docs/basics/object-oriented/variable.md @@ -25,3 +25,11 @@ Java中共有三种变量,分别是类变量、成员变量和局部变量。 } ``` 上面定义的三个变量中,变量a就是类变量,变量b就是成员变量,而变量c和d是局部变量。 + +a作为类变量,他存放在方法区中;b作为成员变量,和对象一起存储在堆内存中(不考虑栈上分配的情况);c和d作为方法的局部变量,保存在栈内存中。 + +之所以要在这一章节重点介绍下这三种变量类型,是因为很多人因为不知道这三种类型的区别,所以不知道他们分别存放在哪里,这就导致不知道那些变量需要考虑并发问题。 + +关于并发问题,目前本书《基本篇》还不涉及,会在下一本《并发篇》中重点介绍,这里先简单说明一下: + +因为只有共享变量才会遇到并发问题,所以,变量a和b是共享变量,变量c和d是非共享变量。所以如果遇到多线程场景,对于变量a和b的操作是需要考虑线程安全的,而对于线程c和d的操作是不需要考虑线程安全的。 \ No newline at end of file diff --git a/docs/basics/object-oriented/why-pass-by-reference.md b/docs/basics/object-oriented/why-pass-by-reference.md index 3f0d890d..81d37c9d 100644 --- a/docs/basics/object-oriented/why-pass-by-reference.md +++ b/docs/basics/object-oriented/why-pass-by-reference.md @@ -1,90 +1,3 @@ -对于初学者来说,要想把这个问题回答正确,最初思考这个问题的时候,我发现我竟然无法通过简单的语言把这个事情描述的很容易理解,遗憾的是,我也没有在网上找到哪篇文章可以把这个事情讲解的通俗易懂。所以,就有了我写这篇文章的初衷。 - -### 辟谣时间 - -关于这个问题,在[StackOverflow][5]上也引发过广泛的讨论,看来很多程序员对于这个问题的理解都不尽相同,甚至很多人理解的是错误的。还有的人可能知道Java中的参数传递是值传递,但是说不出来为什么。 - -在开始深入讲解之前,有必要纠正一下大家以前的那些错误看法了。如果你有以下想法,那么你有必要好好阅读本文。 - -> 错误理解一:值传递和引用传递,区分的条件是传递的内容,如果是个值,就是值传递。如果是个引用,就是引用传递。 -> -> 错误理解二:Java是引用传递。 -> -> 错误理解三:传递的参数如果是普通类型,那就是值传递,如果是对象,那就是引用传递。 - -### 实参与形参 - -我们都知道,在Java中定义方法的时候是可以定义参数的。比如Java中的main方法,`public static void main(String[] args)`,这里面的args就是参数。参数在程序语言中分为形式参数和实际参数。 - -> 形式参数:是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传入的参数。 -> -> 实际参数:在调用有参函数时,主调函数和被调函数之间有数据传递关系。在主调函数中调用一个函数时,函数名后面括号中的参数称为“实际参数”。 - -简单举个例子: - - public static void main(String[] args) { - ParamTest pt = new ParamTest(); - pt.sout("Hollis");//实际参数为 Hollis - } - - public void sout(String name) { //形式参数为 name - System.out.println(name); - } - - -实际参数是调用有参方法的时候真正传递的内容,而形式参数是用于接收实参内容的参数。 - -### 求值策略 - -我们说当进行方法调用的时候,需要把实际参数传递给形式参数,那么传递的过程中到底传递的是什么东西呢? - -这其实是程序设计中**求值策略(Evaluation strategies)**的概念。 - -在计算机科学中,求值策略是确定编程语言中表达式的求值的一组(通常确定性的)规则。求值策略定义何时和以何种顺序求值给函数的实际参数、什么时候把它们代换入函数、和代换以何种形式发生。 - -求值策略分为两大基本类,基于如何处理给函数的实际参数,分为严格的和非严格的。 - -#### 严格求值 - -在“严格求值”中,函数调用过程中,给函数的实际参数总是在应用这个函数之前求值。多数现存编程语言对函数都使用严格求值。所以,我们本文只关注严格求值。 - -在严格求值中有几个关键的求值策略是我们比较关心的,那就是**传值调用**(Call by value)、**传引用调用**(Call by reference)以及**传共享对象调用**(Call by sharing)。 - -* 传值调用(值传递) - * 在传值调用中,实际参数先被求值,然后其值通过复制,被传递给被调函数的形式参数。因为形式参数拿到的只是一个"局部拷贝",所以如果在被调函数中改变了形式参数的值,并不会改变实际参数的值。 -* 传引用调用(引用传递) - * 在传引用调用中,传递给函数的是它的实际参数的隐式引用而不是实参的拷贝。因为传递的是引用,所以,如果在被调函数中改变了形式参数的值,改变对于调用者来说是可见的。 -* 传共享对象调用(共享对象传递) - * 传共享对象调用中,先获取到实际参数的地址,然后将其复制,并把该地址的拷贝传递给被调函数的形式参数。因为参数的地址都指向同一个对象,所以我们也称之为"传共享对象",所以,如果在被调函数中改变了形式参数的值,调用者是可以看到这种变化的。 - -不知道大家有没有发现,其实传共享对象调用和传值调用的过程几乎是一样的,都是进行"求值"、"拷贝"、"传递"。你品,你细品。 - -![][1] - -但是,传共享对象调用和内传引用调用的结果又是一样的,都是在被调函数中如果改变参数的内容,那么这种改变也会对调用者有影响。你再品,你再细品。 - -那么,共享对象传递和值传递以及引用传递之间到底有很么关系呢? - -对于这个问题,我们应该关注过程,而不是结果,**因为传共享对象调用的过程和传值调用的过程是一样的,而且都有一步关键的操作,那就是"复制",所以,通常我们认为传共享对象调用是传值调用的特例** - -我们先把传共享对象调用放在一边,我们再来回顾下传值调用和传引用调用的主要区别: - -**传值调用是指在调用函数时将实际参数`复制`一份传递到函数中,传引用调用是指在调用函数时将实际参数的引用`直接`传递到函数中。** - -![pass-by-reference-vs-pass-by-value-animation][2] - -所以,两者的最主要区别就是是直接传递的,还是传递的是一个副本。 - -这里我们来举一个形象的例子。再来深入理解一下传值调用和传引用调用: - -你有一把钥匙,当你的朋友想要去你家的时候,如果你`直接`把你的钥匙给他了,这就是引用传递。 - -这种情况下,如果他对这把钥匙做了什么事情,比如他在钥匙上刻下了自己名字,那么这把钥匙还给你的时候,你自己的钥匙上也会多出他刻的名字。 - -你有一把钥匙,当你的朋友想要去你家的时候,你`复刻`了一把新钥匙给他,自己的还在自己手里,这就是值传递。 - -这种情况下,他对这把钥匙做什么都不会影响你手里的这把钥匙。 - ### Java的求值策略 前面我们介绍过了传值调用、传引用调用以及传值调用的特例传共享对象调用,那么,Java中是采用的哪种求值策略呢? @@ -159,7 +72,7 @@ pass2 -在参数传递的过程中,实际参数的地址`0X1213456`被拷贝给了形参。这个过程其实就是值传递,只不过传递的值的内容是对象的引用。 +在参数传递的过程中,实际参数的地址`0X1213456`被拷贝给了形参。这个过程其实就是值传递,只不过传递的值得内容是对象的应用。 那为什么我们改了user中的属性的值,却对原来的user产生了影响呢? @@ -220,9 +133,7 @@ OK,以上就是本文的全部内容,不知道本文是否帮助你解开了 [Passing by Value vs. by Reference Visual Explanation][6] - [1]: https://www.hollischuang.com/wp-content/uploads/2020/04/15865905252659.jpg - [2]: https://www.hollischuang.com/wp-content/uploads/2020/04/pass-by-reference-vs-pass-by-value-animation.gif [3]: https://docs.oracle.com/javase/tutorial/java/javaOO/arguments.html [4]: https://en.wikipedia.org/wiki/Evaluation_strategy [5]: https://stackoverflow.com/questions/40480/is-java-pass-by-reference-or-pass-by-value - [6]: https://blog.penjee.com/passing-by-value-vs-by-reference-java-graphical/ + [6]: https://blog.penjee.com/passing-by-value-vs-by-reference-java-graphical/ \ No newline at end of file