Java面向对象
将问题分解成一个一个步骤,对每个步骤进行相应的抽象,形成对象,通过不同对象之间的调用,组合解决问题。
就是说,在进行面向对象进行编程的时候,要把属性、行为等封装成对象,然后基于这些对象及对象的能力进行业务逻辑的实现。
比如:想要造一辆车,上来要先把车的各种属性定义出来,然后抽象成一个Car类。
面向对象具有三大基本特征和五大基本原则
三大基本特征
封装(Encapsulation)
所谓封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
简单的说,一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。
继承(Inheritance)
继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。继承的过程,就是从一般到特殊的过程。
多态(Polymorphism)
所谓多态就是指一个类实例的相同方法在不同情形有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。
这意味着,虽然针对不同对象的具体操作不同,但通过一个公共的类,它们(那些操作)可以通过相同的方式予以调用。
最常见的多态就是将子类传入父类参数中,运行时调用父类方法时通过传入的子类决定具体的内部结构或行为。
面向对象的五大基本原则
单一职责原则(Single-Responsibility Principle)
其核心思想为:一个类,最好只做一件事,只有一个引起它的变化。单一职责原则可以看做是低耦合、高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。
通常意义下的单一职责,就是指只有一种单一功能,不要为类实现过多的功能点,以保证实体只有一个引起它变化的原因。
开放封闭原则(Open-Closed Principle)
其核心思想是:软件实体应该是可扩展的,而不可修改的。也就是,对扩展开放,对修改封闭的。开放封闭原则主要体现在两个方面1、对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。2、对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对其进行任何尝试的修改。
实现开放封闭原则的核心思想就是对抽象编程,而不对具体编程,因为抽象相对稳定。让类依赖于固定的抽象,所以修改就是封闭的;而通过面向对象的继承和多态机制,又可以实现对抽象类的继承,通过覆写其方法来改变固有行为,实现新的拓展方法,所以就是开放的。
Liskov替换原则(Liskov-Substitution Principle)
核心思想是:子类必须能够替换其基类。这一思想体现为对继承机制的约束规范,只有子类能够替换基类时,才能保证系统在运行期内识别子类,这是保证继承复用的基础。在父类和子类的具体行为中,必须严格把握继承层次中的关系和特征,将基类替换为子类,程序的行为不会发生任何变化。同时,这一约束反过来则是不成立的,子类可以替换基类,但是基类不一定能替换子类。
实现的方法是面向接口编程:将公共部分抽象为基类接口或抽象类,通过Extract Abstract Class,在子类中通过覆写父类的方法实现新的方式支持同样的职责。 Liskov替换原则是关于继承机制的设计原则,违反了Liskov替换原则就必然导致违反开放封闭原则。 Liskov替换原则能够保证系统具有良好的拓展性,同时实现基于多态的抽象机制,能够减少代码冗余,避免运行期的类型判别。
依赖倒置原则(Dependency-Inversion Principle)
核心思想是:依赖于抽象。具体而言就是高层模块不依赖于底层模块,二者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象。 我们知道,依赖一定会存在于类与类、模块与模块之间。当两个模块之间存在紧密的耦合关系时,最好的方法就是分离接口和实现:在依赖之间定义一个抽象的接口使得高层模块调用接口,而底层模块实现接口的定义,以此来有效控制耦合关系,达到依赖于抽象的设计目标。
抽象的稳定性决定了系统的稳定性,因为抽象是不变的,依赖于抽象是面向对象设计的精髓,也是依赖倒置原则的核心。 依赖于抽象是一个通用的原则,而某些时候依赖于细节则是在所难免的,必须权衡在抽象和具体之间的取舍,方法不是一层不变的。依赖于抽象,就是对接口编程,不要对实现编程。
接口隔离原则(Interface-Segregation Principle)
核心思想是:使用多个小的专门的接口,而不要使用一个大的总接口。 具体而言,接口隔离原则体现在:接口应该是内聚的,应该避免“胖”接口。一个类对另外一个类的依赖应该建立在最小的接口上,不要强迫依赖不用的方法,这是一种接口污染。
分离的手段主要有以下两种:1、委托分离,通过增加一个新的类型来委托客户的请求,隔离客户和接口的直接依赖,但是会增加系统的开销。2、多重继承分离,通过接口多继承来实现客户的需求,这种方式是较好的。
封装、继承、多态
多态
多态就是同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。按照这个概念来定义的话,多态是一种运行期的状态。
为了实现运行期的多态,或者说动态绑定,需要满足三个条件:
- 有类继承或者接口实现
- 子类要重写父类的方法
- 父类的引用指向子类的对象
1 | public class Parent { |
这样就实现了多态,同样是Parent类的实例,p和p1分别调用不同类的实现。
在这个示例中,我们已知p是son,p1是daughter。但是,有些时候你所使用的对象并不都是自己声明的。
比如Spring中的IOC出来的对象,在使用的时候就不知道它是谁,但是你可以不用关心它是谁
IOC是Ioc-Inversion of Control的缩写,即控制反转,是一种设计思想,意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。即,当我们使用
Spring框架时,对象是Spring容器创建出来并由容器进行管理,我们只需要使用就行了。
方法重写与重载

重写:指的是在Java的子类与父类中有两个名称、参数列表都相同的方法的情况。由于他们具有相同的方法签名,所以子类中的新方法将覆盖父类中原有的方法。
重载:函数或者方法有同样的名称,但是参数列表不相同的情形,这样的同名不同参数的函数或者方法之间,互相称之为重载函数或者方法。
重载VS重写
- 重载是一个编译期概念、重写是一个运行期概念
- 重载遵循所谓“编译期绑定”,即在编译时根据参数变量的类型判断应该调用哪个方法
- 重写遵循所谓“运行期绑定”,即在运行的时候,根据引用变量所指向的实际对象的类型来调用方法
- 因为在编译期已经确定调用哪个方法,所以重载并不是多态。而重写是多态。重载只是一种语言特性,是一种语法规则,与多态无关,与面向对象也无关
重写的例子
1 | class Dog{ |
上面的例子中,dog对象被定义为Dog类型。在编译期,编译器会检查Dog类中是否有可访问的bark()方法,只要其中包含bark()方法,那么就可以编译通过。在运行期,Hound对象被new出来,并赋值给dog变量,这时,JVM是明确的知道dog变量指向的其实是Hound对象的引用。所以,当dog调用bark()方法的时候,就会调用Hound类中定义的bark()方法。这就是所谓的动态多态性。
重写的条件
- 参数列表、返回类型必须完全与被重写方法的相同
- 访问级别的限制性一定不能比被重写方法的强
- 重写方法一定不能抛出新的检查异常或比被重写的方法声明的检查异常更广泛的检查异常
- 不能重写被标示为final的方法
- 如果不能继承一个方法,则不能重写这个方法
重载的例子
1 | class Dog{ |
上面的代码中,定义了两个bark方法,一个是没有参数的bark方法,另外一个是包含一个int类型参数的bark方法。在编译期,编译期可以根据方法签名(方法名和参数情况)情况确定哪个方法被调用。
重载的条件
- 被重载的方法必须改变参数列表
- 被重载的方法可以改变返回类型、访问修饰符、访问修饰符
- 方法能够在同一个类中或者在一个子类中被重载
Java的继承与实现
继承可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。这种派生方式体现了传递性,在Java中,除了继承,还有一种提现传递性的方式叫做实现。
继承和实现两者的明确定义和区别如下:
继承:如果多个类的某个部分的功能相同,那么可以抽象出一个类出来,把他们的相同部分都放到父类里,让他们都继承这个类。
实现:如果多个类处理的目标是一样的,但是处理的方法方式不同,那么就定义一个接口,也就是一个标准,让他们的实现这个接口,各自实现自己具体的处理方法来处理那个目标
继承指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力。所以,继承的根本原因是因为要复用,而实现的根本原因是需要定义一个标准。
在Java中,继承使用extends关键字实现,而实现通过implements关键字
Java为什么不支持多继承
一个类,只有一个父类的情况,我们叫做单继承。而一个类,同时有多个父类的情况,叫做多继承。
在Java中,一个类,只能通过extends关键字继承一个类,不允许多继承。但是,多继承在其他的面向对象语言中是有可能支持的。
像C++就是支持多继承的,主要是因为编程的过程是对现实世界的一种抽象,而现实世界中,确实存在着需要多继承的情况。
例如,可以创造一个“哺乳类动物”类别,拥有进食、繁殖等的功能;然后定义一个子类型“猫”,它可以从父类继承上述功能。
但是,”猫”还可以作为”宠物”的子类,拥有一些宠物独有的能力。
所以,有些面向对象语言是支持多重继承的。
但是,多年以来,多重继承一直都是一个敏感的话题,反对者指它增加了程序的复杂性与含糊性。
菱形继承问题
假设我们有类B和类C,它们都继承了相同的类A。另外我们还有类D,类D通过多重继承机制继承了类B和类C。

这时候,因为D同时继承了B和C,并且B和C又同时继承了A,那么,D中就会因为多重继承,继承到两份来自A中的属性和方法。
这时候,在使用D的时候,如果想要调用一个定义在A中的方法时,就会出现歧义。
因为这样的继承关系的形状类似于菱形,因此这个问题被形象地称为菱形继承问题。
而C++为了解决菱形继承问题,又引入了虚继承。
因为支持多继承,引入了菱形继承问题,又因为要解决菱形继承问题,引入了虚继承。而经过分析,人们发现我们其实真正想要使用多继承的情况并不多。
所以,在 Java 中,不允许“实现多继承”,即一个类不允许继承多个父类。但是 Java 允许“声明多继承”,即一个类可以实现多个接口,一个接口也可以继承多个父接口。由于接口只允许有方法声明而不允许有方法实现(Java 8以前),这就避免了 C++ 中多继承的歧义问题。
Java的继承与组合
遇到想要复用的场景就直接使用继承,这样做是不对的。长期大量的使用继承会给代码带来很高的维护成本。
面向对象的复用技术
Java代码的复用有继承,组合以及代理三种具体的表现形式。下面介绍继承复用和组合复用
继承是类与类或者接口与接口之间最常见的一种关系;继承是一种is-a关系
继承是类与类或者接口与接口之间最常见的一种关系;继承是一种
is-a关系
组合(Composition)体现的是整体与部分、拥有的关系,即has-a的关系
has-a:表示”有一个”的关系,如狗有一个尾巴
组合与继承的区别和联系
在
继承结构中,父类的内部细节对于子类是可见的。所以我们通常也可以说通过继承的代码复用是一种白盒式代码复用。(如果基类的实现发生改变,那么派生类的实现也将随之改变。这样就导致了子类行为的不可预知性;)
组合是通过对现有的对象进行拼装(组合)产生新的、更复杂的功能。因为在对象之间,各自的内部细节是不可见的,所以我们也说这种方式的代码复用是黑盒式代码复用。(因为组合中一般都定义一个类型,所以在编译期根本不知道具体会调用哪个实现类的方法)
继承,在写代码的时候就要指名具体继承哪个类,所以,在编译期就确定了关系。(从基类继承来的实现是无法在运行期动态改变的,因此降低了应用的灵活性。)
组合,在写代码的时候可以采用面向接口编程。所以,类的组合关系一般在运行期确定。
构造函数与默认构造函数
构造函数,是一种特殊的方法。 主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。
特别的一个类可以有多个构造函数,可根据其参数个数的不同或参数类型的不同来区分它们即构造函数的重载。
构造函数跟一般的实例方法十分相似;但是与其它方法不同,构造器没有返回类型,不会被继承,且可以有范围修饰符。
构造器的函数名称必须和它所属的类的名称相同。它承担着初始化对象数据成员的任务。
如果在编写一个可实例化的类时没有专门编写构造函数,多数编程语言会自动生成缺省构造器(默认构造函数)。默认构造函数一般会把成员变量的值初始化为默认值,如int -> 0,Integer -> null。
但是,如果我们手动在某个类中定义了一个有参数的构造函数,那么这个默认的无参构造函数就不会自动添加了。需要手动创建。
类变量、成员变量和局部变量
Java中共有三种变量,分别是类变量、成员变量和局部变量。他们分别存放在JVM的方法区、堆内存和栈内存中。
1 | public class Variables { |
成员变量和方法作用域
对于成员变量和方法的作用域,public,protected,private以及不写之间的区别:
public:表明该成员变量或者方法是对所有类或者对象都是可见的,所有类或者对象都可以直接访问
private:表明该成员变量或者方法是私有的,只有当前类对其具有访问权限,除此之外其他类或者对象都没有访问权限.子类也没有访问权限.
protected:表明成员变量或者方法对类自身,与同在一个包中的其他类可见,其他包下的类不可访问,除非是他的子类
default:表明该成员变量或者方法只有自己和其位于同一个包的内可见,其他包内的类不能访问,即便是它的子类
平台无关性
平台无关性就是一种语言在计算机上的运行不受平台的约束,一次编译,到处执行(Write Once ,Run Anywhere)。
也就是说,用Java创建的可执行二进制程序,能够不加改变的运行于多个平台。
平台无关性的实现
对于Java的平台无关性的支持,就像对安全性和网络移动性的支持一样,是分布在整个Java体系结构中的。其中扮演者重要的角色的有Java语言规范、Class文件、Java虚拟机(JVM)等。
编译原理基础
在Java平台中,想要把Java文件,编译成二进制文件,需要经过两步编译,前端编译和后端编译:

前端编译主要指与源语言有关但与目标机无关的部分。Java中,我们所熟知的javac的编译就是前端编译。除了这种以外,我们使用的很多IDE,如eclipse,idea等,都内置了前端编译器。主要功能就是把.java代码转换成.class代码。
后端编译主要是将中间代码再翻译成机器语言。Java中,这一步骤就是Java虚拟机来执行的。

所以,我们说的,Java的平台无关性实现主要作用于以上阶段。如下图所示:

JVM
所谓平台无关性,就是说要能够做到可以在多个平台上都能无缝对接。但是,对于不同的平台,硬件和操作系统肯定都是不一样的。
对于不同的硬件和操作系统,最主要的区别就是指令不同。比如同样执行a+b,A操作系统对应的二进制指令可能是10001000,而B操作系统对应的指令可能是11101110。那么,想要做到跨平台,最重要的就是可以根据对应的硬件和操作系统生成对应的二进制指令。
而这一工作,主要由我们的Java虚拟机完成。虽然Java语言是平台无关的,但是JVM却是平台有关的,不同的操作系统上面要安装对应的JVM。
有了Java虚拟机,想要执行a+b操作,A操作系统上面的虚拟机就会把指令翻译成10001000,B操作系统上面的虚拟机就会把指令翻译成11101110。

所以,Java之所以可以做到跨平台,是因为Java虚拟机充当了桥梁。他扮演了运行时Java程序与其下的硬件和操作系统之间的缓冲角色。
字节码
各种不同的平台的虚拟机都使用统一的程序存储格式——字节码(ByteCode)是构成平台无关性的另一个基石。Java虚拟机只与由字节码组成的Class文件进行交互。
我们说Java语言可以Write Once ,Run Anywhere。这里的Write其实指的就是生成Class文件的过程。
因为Java Class文件可以在任何平台创建,也可以被任何平台的Java虚拟机装载并执行,所以才有了Java的平台无关性。
Java语言规范
已经有了统一的Class文件,以及可以在不同平台上将Class文件翻译成对应的二进制文件的Java虚拟机,Java就可以彻底实现跨平台了吗?
其实并不是的,Java语言在跨平台方面也是做了一些努力的,这些努力被定义在Java语言规范中。
比如,Java中基本数据类型的值域和行为都是由其自己定义的。而C/C++中,基本数据类型是由它的占位宽度决定的,占位宽度则是由所在平台决定的。所以,在不同的平台中,对于同一个C++程序的编译结果会出现不同的行为。
举一个简单的例子,对于int类型,在Java中,int占4个字节,这是固定的。
但是在C++中却不是固定的了。在16位计算机上,int类型的长度可能为两字节;在32位计算机上,可能为4字节;当64位计算机流行起来后,int类型的长度可能会达到8字节。(这里说的都是可能)
通过保证基本数据类型在所有平台的一致性,Java语言为平台无关性提供强了有力的支持。
小结
- Java语言规范:通过规定
Java语言中基本数据类型的取值范围和行为 - Class文件:所有Java文件都要编译成统一的Class文件
- Java虚拟机:将Class文件转成对应平台的二进制文件等
Java的平台无关性是建立在Java虚拟机的平台有关性基础之上的,是因为Java虚拟机屏蔽了底层操作系统和硬件的差异