我们都知道Java是一门面向对象的语言。什么是面向对象,它有什么特性呢,今天我们就来说一下这个"面向对象"到底是什么意思。
面向对象简称 OO(Object Oriented),20 世纪 80 年代以后,其实就有了面向对象分析(OOA)、 面向对象设计(OOD)、面向对象程序设计(OOP)等新的系统开发方式模型的研究。对面对对象设计的语言来说,一切皆是对象。把现实世界中的对象抽象地体现在编程世界中,比如一个苹果我们可以new Apple(),一个小狗我们可以new Dog(),一个对象代表了某个具体的操作。而一个个对象最终组成了完整的程序设计,这些对象可以是独立存在的,也可以是从别的对象继承过来的。对象之间通过相互作用传递信息,从而使用简单的对象和对象的关系,实现庞大而又复杂的程序开发。
什么是类
类是对现实生活中一类具有共同特征的事物的抽象。比如我们都是人类,小狗是动物类,小花是植物类。如果一个程序里提供的数据类型与应用中的概念有直接的对应,这个程序就会更容易理解,也更容易修改。一组经过很好选择的用户定义的类会使程序更简洁。
在Java类的内部封装了了属性和方法,用于操作自身的成员。类是对某种对象的抽象定义,具有行为,它描述一个对象能够做什么以及做的方法(method),它们是可以对这个对象进行操作的程序和过程。它包含有关对象行为方式的信息,包括它的名称、属性、方法和事件。
/**
* @desc 定义一个人类
* @author dataozi
* @date 2020/6/7 15:24
*/
@Getter
@Setter
@NoArgsConstructor
public class Person {
// 人的名字
private String name;
// 人的年龄
private Integer age;
/**
* @desc 人会吃饭
* @author dataozi
* @date 2020/6/7 15:26
*/
public void eat(){
}
/**
* @desc 人会跑
* @author dataozi
* @date 2020/6/7 15:26
*/
public void run(){
}
}
什么是对象
Java 是面向对象的编程语言,对象就是面向对象程序设计的核心。那么什么是对象呢?所谓对象就是真实世界中的实体,我们通过一种语言设计,将真实世界的事物通过程序中的对象表现出来,也就是对象与实体是一一对应的,现实世界中每一个实体都是一个对象,它是一种具体的概念。通常,对象有以下特点:
- 对象具有属性和行为。
- 对象具有变化的状态。
- 对象具有唯一性。
- 对象都是某个类别的实例。
- 一切皆为对象,真实世界中的所有事物都可以视为对象。
拿自己来说,把我对应上一个对象的话,那么像我的身高、体重、年龄等都是我的属性,而我会吃饭、说话、走路等这些就是我的行为;我会长大,我也会拥有很多角色,我的状态会发生变化;全世界只有一个我(顿时自豪感爆棚,有没有);我是男生类中的一员,我是程序猿类中的一员。
@Test
public void testForOO(){
// 我是人类中的一个真实的对象
Person dataozi = new Person();
// 我的名字叫大套子
dataozi.setName("大套子");
// 我年年都18
dataozi.setAge(18);
// 我是人,所以我会吃
dataozi.eat();
}
面向对象和面向过程的区别
上边我们了解到面向对象编程语言的特点,及面向的是一个个的对象,对象是整个程序设计的核心。那么面向过程又是什么呢?面向过程的语言也称为结构化程序设计语言,是高级语言的一种。在面向过程程序设计中,问题被看作一系列需要完成的任务,函数则用于完成这些任务,解决问题的焦点集中于函数。它的主要观点是采用自顶向下、逐步求精的程序设计方法,使用三种基本控制结构构造程序,即任何程序都可由顺序、选择、循环三种基本控制结构构造。面向过程语言程序设计过程就是用一系列语句描述问题解决过程中的一系列步骤的过程【摘自百度百科】。简单来说就是该思想是站着过程的角度思考问题,强调的就是功能行为,功能的执行过程,即先后顺序,而每一个功能我们都使用函数(类似于方法)把这些步骤一步一步实现。使用的时候依次调用函数就可以了。
面向过程:
- 性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
- 最小的程序单元是函数,每个函数负责完成某一个功能,用于接受输入数据,函数对输入数据进行处理,然后输出结果数据,整个软件系统由一个个的函数组成,其中作为程序入口的函数称之为主函数,主函数依次调用其他函数,普通函数之间可以相互调用,从而实现整个系统功能。
- 自顶而下的设计模式,在设计阶段就需要考虑每一个模块应该分解成哪些子模块,每一个子模块又细分为更小的子模块,如此类推,直到将模块细化为一个个函数。
- 设计不够直观,与人类的思维习惯不一致 系统软件适应新差,可拓展性差,维护性低、
面向对象:
- 面向对象最小的程序单元是:类。面向对象更加符合常规的思维方式,稳定性好,可重用性强,易于开发大型软件产品,有良好的可维护性。
- 易维护,采用面向对象思想设计的结构,可读性高,由于继承的存在,即使改变需求,那么维护也只是在局部模块,所以维护起来是非常方便和较低成本的。
- 效率高,在软件开发时,根据设计的需要对现实世界的事物进行抽象,产生类。使用这样的方法解决问题,接近于日常生活和自然的思考方式,势必提高软件开发的效率和质量。
- 易扩展,由于继承、封装、多态的特性,自然设计出高内聚、低耦合的系统结构,使得系统更灵活、更容易扩展,而且成本较低。
面向对象的三大特性
面向对象开发模式更有利于人们开拓思维,在具体的开发过程中便于程序的划分,方便程序员分工合作,提高开发效率。
该开发模式之所以使程序设计更加完善和强大,主要是因为面向对象具有继承、封装和多态 3 个核心特性。
封装
1、概念
封装有点武装的意思,将我们本身很重要的信息封装起来,不对外界进行暴露,从而是信息更加安全,就像取款机一样,银行把钱放到取款机里,取款机对我们来说就是一个封闭的私有的对象,它向我们提供了一个公共的出口,我们只能看到这个公共的功能,而它里边到底装了多少钱,我们是不知道的,在面向对象程式设计方法中,封装就是将抽象性函式接口的实现细节部份包装、隐藏起来的方法。
- 封装可以被认为是一个保护屏障,防止该类的代码和数据被外部类定义的代码随机访问。
- 要访问该类的代码和数据,必须通过严格的接口控制。
- 封装最主要的功能在于我们能修改自己的实现代码,而不用修改那些调用我们代码的程序片段。
- 适当的封装可以让程式码更容易理解与维护,也加强了程式码的安全性。
- 保护类中的信息,它可以阻止在外部定义的代码随意访问内部代码和数据。
- 隐藏细节信息,一些不需要程序员修改和使用的信息,比如取款机中的键盘,用户只需要知道按哪个键实现什么操作就可以,至于它内部是如何运行的,用户不需要知道。
- 有助于建立各个系统之间的松耦合关系,提高系统的独立性。当一个系统的实现方式发生变化时,只要它的接口不变,就不会影响其他系统的使用。例如 U 盘,不管里面的存储方式怎么改变,只要 U 盘上的 USB 接口不变,就不会影响用户的正常操作。
- 提高软件的复用率,降低成本。每个系统都是一个相对独立的整体,可以在不同的环境中得到使用。例如,一个 U 盘可以在多台电脑上使用。
Java 语言的基本封装单位是类。由于类的用途是封装复杂性,所以类的内部有隐藏实现复杂性的机制。Java 提供了私有和公有的访问模式,类的公有接口代表外部的用户应该知道或可以知道的每件东西,私有的方法数据只能通过该类的成员代码来访问,这就可以确保不会发生不希望的事情。
2、优点
- 良好的封装能够减少耦合。
- 类内部的结构可以自由修改。
- 可以对成员变量进行更精确的控制。
- 隐藏信息,实现细节。
3、访问修饰符
Java中通过访问修饰符对类中的属性和方法进行修饰,从而对对应的属性和方法赋予相应的访问权限控制。
访问修饰符范围
修饰符 | 同类 | 同包 | 子类 | 其它 |
private | √ | | | |
默认 | √ | √ | | |
protected | √ | √ | √ | |
public | √ | √ | √ | √ |
当我们对某个类中的属性用private进行修饰,然后提供public的get()/set()方法时,我们其实对该类的属性进行了封装,因为其它类无法直接访问到这些属性,只能通过公共的方法获取或设置。
this关键字
关于this关键字详情点击这里
内部类
内部类( Inner Class )就是定义在另外一个类里面的类。与之对应,包含内部类的类被称为外部类。
那么问题来了:那为什么要将一个类定义在另一个类里面呢?清清爽爽的独立的一个类多好啊!!
内部类的主要作用如下:
1. 内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类。
2. 内部类的方法可以直接访问外部类的所有数据,包括私有的数据。
3. 内部类所实现的功能使用外部类同样可以实现,只是有时使用内部类更方便。
内部类可分为以下几种:
- 成员内部类
- 静态内部类
- 方法内部类
- 匿名内部类
关于内部类,详情点击这里
继承
1、概念
继承这个词我们在生活中也经常听到,新闻报道某小伙不愿继承其父亲的万亩养猪场跑到城市打工。就像儿子继承老子的财务一样,在Java中继承则表示子类可以继承父类的属性和方法,继承是java面向对象编程技术的一块基石,因为它允许创建分等级层次的类。Java中只允许单继承,及一个类继承了一个类,则无法同时继承其它类。
看下下面代码:
/**
* @desc 定义一个男孩类
* @author dataozi
* @date 2020/6/7 16:00
*/
public class Boy {
// 男孩的名字
private String name;
// 男孩的年龄
private Integer age;
/**
* @desc 男孩会吃饭
* @author dataozi
* @date 2020/6/7 15:26
*/
public void eat(){
}
/**
* @desc 男孩会跑
* @author dataozi
* @date 2020/6/7 15:26
*/
public void run(){
}
}
/**
* @desc 定义一个女孩类
* @author dataozi
* @date 2020/6/7 16:00
*/
public class Girl {
// 女孩的名字
private String name;
// 女孩的年龄
private Integer age;
/**
* @desc 女孩会吃饭
* @author dataozi
* @date 2020/6/7 15:26
*/
public void eat(){
}
/**
* @desc 女孩会跑
* @author dataozi
* @date 2020/6/7 15:26
*/
public void run(){
}
}
从上面代码中可以看到,男孩类和女孩类都有名字和年龄的属性,都有吃饭、跑步的方法。但是我们在两个类中都进行了声明,这样显得很重复。所以我们尝试将男孩和女孩公有的属性和方法进行抽取,声明一个人类,叫男孩和女孩分别继承这个人类,使用这种层次形的分类方式,是为了将多个类的通用属性和方法提取出来,放在它们的父类中,然后只需要在子类中各自定义自己独有的属性和方法,并以继承的形式在父类中获取它们的通用属性和方法即可。
继承是类与类的一种关系,是一种“is a”的关系。比如“狗”继承“动物”,这里动物类是狗类的父类或者基类,狗类是动物类的子类或者派生类。Java中使用extends实现继承。
/**
* @desc 定义一个男孩类
* @author dataozi
* @date 2020/6/7 16:00
*/
public class Boy extends Person{
}
/**
* @desc 定义一个女孩类
* @author dataozi
* @date 2020/6/7 16:00
*/
public class Girl extends Person{
}
@Test
public void testForOO(){
// 我是人类中的一个真实的对象
Boy dataozi = new Boy();
// 我的名字叫大套子
dataozi.setName("大套子");
// 我年年都18
dataozi.setAge(18);
// 我是人,所以我会吃
dataozi.eat();
}
可以看到,虽然Boy类没有任何属性和方法,但是因为它继承了Person类,所以Boy类拥有了Person类的属性和方法。
补充:Java中的继承只能单继承,但是可以通过内部类继承其他类来实现多继承。
public class Son extends Father{
public void go () {
System.out.println("son go");
}
public void eat () {
System.out.println("son eat");
}
public void sleep() {
System.out.println("zzzzzz");
}
public void cook() {
//匿名内部类实现的多继承
new Mother().cook();
//内部类继承第二个父类来实现多继承
Mom mom = new Mom();
mom.cook();
}
private class Mom extends Mother {
@Override
public void cook() {
System.out.println("mom cook");
}
}
}
2、优点
- 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
- 提高代码的重用性;
- 子类可以形似父类,但又异于父类,“龙生龙,凤生凤,老鼠生来会打洞”是说子拥有 父的“种”,“世界上没有两片完全相同的叶子”是指明子与父的不同;
- 提高代码的可扩展性,实现父类的方法就可以“为所欲为”了,君不见很多开源框架的扩展接口都是通过继承父类来完成的;
3、重写
子类如果对继承的父类的方法不满意(不适合),可以自己编写继承的方法,这种方式就称为方法的重写。当调用方法时会优先调用子类的方法。
重写要注意:
a、返回值类型
b、方法名
c、参数类型及个数
都要与父类继承的方法相同,才叫方法的重写。
重载和重写的区别:
方法重载:在同一个类中处理不同数据的多个相同方法名的多态手段。
方法重写:相对继承而言,子类中对父类已经存在的方法进行区别化的修改。
4、继承的初始化顺序
1、初始化父类再初始化子类
2、先执行初始化对象中属性,再执行构造方法中的初始化。
基于上面两点,我们就知道实例化一个子类,java程序的执行顺序是:
父类对象属性初始化---->父类对象构造方法---->子类对象属性初始化--->子类对象构造方法
5、final关键字
使用final关键字做标识有“最终的”含义。
1. final 修饰类,则该类不允许被继承。
2. final 修饰方法,则该方法不允许被覆盖(重写)。
3. final 修饰属性,则该类的该属性不会进行隐式的初始化,所以 该final 属性的初始化属性必须有值,或在**构造方法中赋值(但只能选其一,且必须选其一,因为没有默认值!),**且初始化之后就不能改了,只能赋值一次。
4. final 修饰变量,则该变量的值只能赋一次值,在声明变量的时候才能赋值,即变为常量。
6、super关键字
在对象的内部使用,可以代表父类对象。
1、访问父类的属性:super.age
2、访问父类的方法:super.eat()
super的应用:
首先我们知道子类的构造的过程当中必须调用父类的构造方法。其实这个过程已经隐式地使用了我们的super关键字。
这是因为如果子类的构造方法中没有显示调用父类的构造方法,则系统默认调用父类无参的构造方法。
那么如果自己用super关键字在子类里调用父类的构造方法,则必须在子类的构造方法中的第一行。
要注意的是:如果子类构造方法中既没有显示调用父类的构造方法,而父类没有无参的构造方法,则编译出错。
(补充说明,虽然没有显示声明父类的无参的构造方法,系统会自动默认生成一个无参构造方法,但是,如果你声明了一个有参的构造方法,而没有声明无参的构造方法,这时系统不会动默认生成一个无参构造方法,此时称为父类有没有无参的构造方法。)
多态
1、概念
面向对象的多态性,即“一个接口,多个方法”。就好比LoL中的Q、W、E、R四个键位一样,同样的键位,当你使用盖伦时R键的技能就是从天而降的大宝剑,当你使用皇子时R键就是从天而降的岩石圈。Java中的多态性体现在父类中定义的属性和方法被子类继承后,可以具有不同的属性或表现方式。多态性允许一个接口被多个同类使用,弥补了单继承的不足。
同一个事件发生在不同的对象上会产生不同的结果。
多态是同一个行为具有多个不同表现形式或形态的能力。
多态就是同一个接口,使用不同的实例而执行不同操作,如图所示:
多态性是对象多种表现形式的体现。
2、重写和重载
多态一般可以分为两种,一个是重写override,一个是重载overload。
重写是由于继承关系中的子类有一个和父类同名同参数的方法,会覆盖掉父类的方法。重载是因为一个同名方法可以传入多个参数组合。
注意,同名方法如果参数相同,即使返回值不同也是不能同时存在的,编译会出错。
从jvm实现的角度来看,重写又叫运行时多态,编译时看不出子类调用的是哪个方法,但是运行时操作数栈会先根据子类的引用去子类的类信息中查找方法,找不到的话再到父类的类信息中查找方法。
而重载则是编译时多态,因为编译期就可以确定传入的参数组合,决定调用的具体方法是哪一个了。
3、向上转型和向下转型
public static void main(String[] args) {
Son son = new Son();
//首先先明确一点,转型指的是左侧引用的改变。
//father引用类型是Father,指向Son实例,就是向上转型,既可以使用子类的方法,也可以使用父类的方法。
//向上转型,此时运行father的方法
Father father = son;
father.smoke();
//不能使用子类独有的方法。
// father.play();编译会报错
father.drive();
//Son类型的引用指向Father的实例,所以是向下转型,不能使用子类非重写的方法,可以使用父类的方法。
//向下转型,此时运行了son的方法
Son son1 = (Son) father;
//转型后就是一个正常的Son实例
son1.play();
son1.drive();
son1.smoke();
//因为向下转型之前必须先经历向上转型。
//在向下转型过程中,分为两种情况:
//情况一:如果父类引用的对象如果引用的是指向的子类对象,
//那么在向下转型的过程中是安全的。也就是编译是不会出错误的。
//因为运行期Son实例确实有这些方法
Father f1 = new Son();
Son s1 = (Son) f1;
s1.smoke();
s1.drive();
s1.play();
//情况二:如果父类引用的对象是父类本身,那么在向下转型的过程中是不安全的,编译不会出错,
//但是运行时会出现java.lang.ClassCastException错误。它可以使用instanceof来避免出错此类错误。
//因为运行期Father实例并没有这些方法。
Father f2 = new Father();
Son s2 = (Son) f2;
s2.drive();
s2.smoke();
s2.play();
//向下转型和向上转型的应用,有些人觉得这个操作没意义,何必先向上转型再向下转型呢,不是多此一举么。其实可以用于方法参数中的类型聚合,然后具体操作再进行分解。
//比如add方法用List引用类型作为参数传入,传入具体类时经历了向下转型
add(new LinkedList());
add(new ArrayList());
//总结
//向上转型和向下转型都是针对引用的转型,是编译期进行的转型,根据引用类型来判断使用哪个方法
//并且在传入方法时会自动进行转型(有需要的话)。运行期将引用指向实例,如果是不安全的转型则会报错。
//若安全则继续执行方法。
}
public static void add(List list) {
System.out.println(list);
//在操作具体集合时又经历了向上转型
// ArrayList arr = (ArrayList) list;
// LinkedList link = (LinkedList) list;
}
总结: 向上转型和向下转型都是针对引用的转型,是编译期进行的转型,根据引用类型来判断使用哪个方法。并且在传入方法时会自动进行转型(有需要的话)。运行期将引用指向实例,如果是不安全的转型则会报错,若安全则继续执行方法。
4、编译期的静态分派
其实就是根据引用类型来调用对应方法。
public static void main(String[] args) {
Father father = new Son();
静态分派 a= new 静态分派();
//编译期确定引用类型为Father。
//所以调用的是第一个方法。
a.play(father);
//向下转型后,引用类型为Son,此时调用第二个方法。
//所以,编译期只确定了引用,运行期再进行实例化。
a.play((Son)father);
//当没有Son引用类型的方法时,会自动向上转型调用第一个方法。
a.smoke(father);
//
}
public void smoke(Father father) {
System.out.println("father smoke");
}
public void play (Father father) {
System.out.println("father");
//father.drive();
}
public void play (Son son) {
System.out.println("son");
//son.drive();
}
5、方法重载优先级匹配
public static void main(String[] args) {
方法重载优先级匹配 a = new 方法重载优先级匹配();
//普通的重载一般就是同名方法不同参数。
//这里我们来讨论当同名方法只有一个参数时的情况。
//此时会调用char参数的方法。
//当没有char参数的方法。会调用int类型的方法,如果没有int就调用long
//即存在一个调用顺序char -> int -> long ->double -> ..。
//当没有基本类型对应的方法时,先自动装箱,调用包装类方法。
//如果没有包装类方法,则调用包装类实现的接口的方法。
//最后再调用持有多个参数的char...方法。
a.eat('a');
a.eat('a','c','b');
}
public void eat(short i) {
System.out.println("short");
}
public void eat(int i) {
System.out.println("int");
}
public void eat(double i) {
System.out.println("double");
}
public void eat(long i) {
System.out.println("long");
}
public void eat(Character c) {
System.out.println("Character");
}
public void eat(Comparable c) {
System.out.println("Comparable");
}
public void eat(char ... c) {
System.out.println(Arrays.toString(c));
System.out.println("...");
}
// public void eat(char i) {
// System.out.println("char");
// }