面向对象编程 OOP
1. 入门便是不识
从我们刚开始接触到 Java 这门语言之后,除了所谓的增删改查,就是会有人告诉我们这是一个面向对象编程的语言,然而说道面向对象,听到最多的就是"不就对象嘛,new 一个不就行了!" ,其实并不知道什么是对象。
现在就赶紧回来看看什么是 OOP 面向对象编程(Object Oriented Programming)。
2. 举例说明什么是面向对象
举个最简单点的例子来区分,面向过程和面向对象。
有一天你想吃鱼香肉丝了,怎么办呢?你有两个选择:
1 、自己买材料,肉,鱼香肉丝调料,蒜苔,胡萝卜等等然后切菜切肉,开炒,盛到盘子里。
2 、去饭店,张开嘴:老板!来一份鱼香肉丝!
看出来区别了吗?在这里 1 是面向过程,2 是面向对象。
面向对象有什么优势呢?首先你不需要知道鱼香肉丝是怎么做的,降低了耦合性。如果你突然不想吃鱼香肉丝了,想吃烤羊排,对于 1 你可能不太容易了,还需要重新买菜,买调料什么的。对于 2 的话太容易了,大喊:老板!那个鱼香肉丝换成烤羊排吧!提高了可维护性。总的来说就是降低耦合,提高维护性!
面向过程是具体化的,流程化的,解决一个问题,你需要一步一步的分析,一步一步的实现。
面向对象是模型化的,你只需抽象出一个类,这是一个封闭的盒子,在这里你拥有数据也拥有解决问题的方法。需要什么功能直接使用就可以了,不必去一步一步的实现,至于这个功能是如何实现的,管我们什么事?我们会用就可以了。
面向对象的底层其实还是面向过程,把面向过程抽象成类,然后封装,方便我们我们使用的就是面向对象了。
2.1 优缺点分析
上面举例描述了什么是面向过程和面向对象,接下来我们总结一下各自的特点。
对于面向过程:
优点:性能比面向对象好,因为调用时需要实例化,开销比较大,比较消耗资源。
缺点:不易维护、不易复用、不易扩展。
对于面向对象:
优点:易维护、易复用、易扩展,由于面向对象有封装、多态、继承的特性,可以设计出低耦合的系统,使系统更加灵活、更加易用维护。
缺点:性能方面比面向过程差。
3. 面向对象的三大特征
面向对象有三大特征,分别是封装、多态和继承。接下来我们来逐个来学习一下。
那么首先我们就要举个例子,对象。
对象就是对事物的一种抽象描述。现实世界中的事物,都可以用"数据" 和"能力" 来描述。
比如我要描述一个人,"数据" 就是他的年龄、性别、身高体重,"能力" 就是他能做什么工作,承担什么样的责任。
描述一个外卖软件,"数据" 就是他包含的菜品,而"能力" 就是他可以点菜。
3.1 面向对象特征-封装
隐藏对象的属性和实现细节,仅对外提供公共访问方式,将变化隔离,便于使用,提高复用性和安全性。
我们把"数据" 和"能力" 组合成一个对象的过程就叫做"封装" 。
封装的结果就是可以有一个类,通过这个类我们可以获得一个对象。然后我们就可以通过给这个对象下达指令,然后让他执行自己的"能力" 。
封装只是面向对象的第一步,目的是把现实世界的东西抽象成对象。面向对象真正有威力的地方是"多态" 和"继承" 。
封装的核心思想是:隐藏实现,暴露接口。
封装的目的:
1 、解耦
2 、保护数据安全
3.1.1 访问修饰权限符
修饰符
权限范围
public
本类,子类,本包,外部包
protected
本类,子类,本包
default
本类,子类
private
本类
3.2 面向对象特征-多态
父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。提高了程序的拓展性。
同一操作,作用于不同的对象,可以产生不同结果,这就是"多态" 。通常说的多态是指运行期的多态,也叫动态绑定。
要实现多态,需要满足三个条件:
1 、有类继承或接口实现;
2 、子类重写父类的方法;
3 、父类的引用指向子类的对象。
比如:
犬科动物 {
吠();
}
狗 继承 犬科动物 {
吠(){
汪汪汪!
}
}
狼 继承 犬科动物 {
吠(){
嗷嗷嗷!
}
}
狗和狼都是犬科动物,拉来一只犬科动物,如果让你来分辨的话,你可能没办法直接分辨出它到底是狗还是狼。只要它真正的叫出来的时候,你才知道。这就是运行是多态。
转化成 Java 代码如下:
public class Canidae {
public void bark () {};
}
public class Dog extends Canidae {
public void bark () {
System.out.println("汪汪汪..." )
}
}
public class Wolf extends Canidae {
public void bark () {
System.out.println("嗷嗷嗷..." )
}
}
public class Main () {
Canidae canidae = new Dog();
Canidae canidae1 = new Wolf();
canidae.bark();
canidae1.bark();
}
这样,就实现了多态,同样的是 Canidae 的实例,canidae.bark() 调用的就是 Dog 类的方法,而 canidae1.bark() 调用的却是 Wolf 的方法。
3.2.1 重写和重载
"重写" 指子类重写父类的方法,方法名、参数列表必须相同,返回值范围小于等于父类,抛出异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为 private 或者 final 则子类就不能重写方法。
"重载" 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。
3.2.2 向上转型
在多态中需要将子类的引用赋给父类对象,只有这样该引用才既能调用父类的方法,又能调用子类的方法。
3.2.3 强制转换
从子类到父类的类型转换可以自动进行。
从父类到子类的类型转换必须通过造型(强制类型转换)实现。
无继承关系的引用类型间的转换是非法的。
3.3 面向对象特征-继承
提高代码复用性;继承是多态的前提。
在面向对象编程中,当两个类具有相同的特征(属性)和行为(方法)时,可以将相同的部分抽取出来放到一个类中作为父类,其他两个类"继承" 这个父类。继承后子类自动拥有了父类的部分属性和方法。
通过继承创建新类称为"子类" 或"派生类" 。
被继承的类称为"基类" 、"父类" 或"超类" 。
比如:
狗 {
吠();
}
牧羊犬 继承 狗 {
放羊();
}
上面的例子中,狗类时父类,牧羊犬是子类。牧羊犬类通过继承获得狗类的 吠() 的能力,同时增加了自己独有的 放羊() 的能力。
转换成 Java 代码如下:
public class Dog {
public void bark () {
System.out.println("汪汪汪..." )
}
}
public class HerdingDog extends Dog {
public void herd () {
System.out.println("放羊中..." )
}
}
public class Main () {
HerdingDog dog = new HerdingDog();
dog.bark();
dog.herd();
}
3.3.1 构造器的继承问题
1 、构造器是不会被子类继承的,但是子类的对象在初始化时会默认调用父类的无参构造器
2 、当父类显示写了有参构造器,且没有无参构造器,子类继承父类的时候必须显示的调用父类的有参构造器。调用的方式可以使用 super (a,b) 来调用。
3.3.2 static 修饰符的继承问题
子类是不会继承父类被 static 修饰的方法和变量,但是可以调用。
3.3.3 super 关键字
super 关键字用于引用使用该关键字的类的父类。
作为独立语句出现的 super 表示调用父类的构造方法。
如果是为了调用父类的成员变量,成员方法是可以直接使用的,不需要借助于 super 。
如果子类中出现了和父类同名的成员变量,成员方法,可以使用 super 关键字告知编译器,这里调用的是父类的成员变量,成员方法。
super 关键字调用构造方法,父类的构造方法
格式:
super (实际参数);
1 . super 调用父类构造方法,是通过参数类型,个数和顺序来确定对应的父类构造方法;
2 . super 调用父类构造方法,必须在当前子类构造方法代码块的第一行;
3 . this 和 super 不能同时出现在同一个子类构造方法中调用构造方法;
4 . Java 编译器会自动选择"隐式" 调用 super () 对应的就是父类的无参构造方法,用于初始化父类的成员变量内存空间。
3.3.4 this 关键字
this 关键字用于引用当前实例。
当引用可能不明确时,可以使用 this 关键字来引用当前的实例。
表示调用当前方法的类对象。
可以利用 this 调用类对象的成员变量和成员方法,可以用于操作当前类对象。
this 关键字调用构造方法:
格式:
this (实际参数);
1 、有且只能用在 Constructor 构造方法内;
2 、this () 在类内调用其他构造方法,是根据小括号内的实际参数类型来选择的;
3 、两个构造方法,不能通过 this 关键字,相互调用,出现语法错误;
4 、this (实际参数)调用其他构造方法,有且只能是在当前构造方法代码块的第一行,并且只能调用一个。
3.3.5 final 关键字
final 修饰类:
不能被继承,例如 String
final 修饰成员方法:
不能被子类或者实现类重写
final 修饰成员变量:
定义时必须初始化,并且初始化之后无法修改
final 修饰局部变量:
赋值之后无法修改
final 标记的变量(成员变量或局部变量)即称为常量。名称大写,且只 能被赋值一次。
3.3.6 子类对象实例化过程
new 出一个子类对象的时候,必须先 new 出一个父类对象。子类在调用构造方法时,会优先调用父类的构造方法。(默认)
如果不写构造方法,编译器默认加上一个无参构造器,如果写了构造器,编译器就不会添加。
如果写了有参构造器,子类就必须调用父类的构造方法。super (参数)。
如果同时有有参和无参构造器,那么会默认调用无参。也可以自定义调用有参。
3.4 简单小结
1 、抽象会使复杂的问题更加简化。
2 、从以前面向过程的执行者,变成了面向对象的指挥者。
3 、面向对象更符合人类的思维,面向过程则是机器的思想。
4. Object 类
4.1 Object 类
1 、Object 类时所有 Java 的根父类
2 、如果在类的声明未使用 extends 关键字指名其父类,则默认父类为 java.lang.Object 类(任何类都可以调用 Object 的方法)
Object 的主要组成:
1 、public native int hashCode ()
取得 hash 码
2、equals (Object obj)
比较对象
3、clone ()
可用于复杂对象的深拷贝
4.2 == 与 equals 的区别
== 既可以比较基本类型也可以比较引用类型。对于基本类型就是比较值,对于引用类型就是比较内存地址。
equals 方法用来比较的是两个对象的内容是否相等,由于所有的类都是继承自 java.lang.Object 类的,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是 Object 类中的方法,而 Object 中的 equals 方法返回的却是 == 的判断。
5. static 关键字
在 Java 类中,可用 static 修饰属性、方法、代码块、内部类。
被修饰后的成员具备以下特点:
1 、修饰的成员,被所有对象所共享;
2 、访问权限允许时,可不创建对象,直接用类名.属性或方法调用。
在 static 方法内部只能访问类的 static 修饰的属性或方法,不能访问类的非 static 的结构。
static 修饰的方法不能被重写。
static 修饰成员变量
1 、保存在内存的数据区;
2 、生存周期是从类文件加载开始,到程序退出结束,生存周期是远远超过类对象的;
3 、一处修改,所有使用到该成员变量的位置都会被影;
4 、调用格式:
类名.静态成员变量 √
static 修饰成员方法
1 、调用格式:
类名.静态成员方法(); √
2 、生存周期是从类文件加载开始,到程序退出结束,整个生存周期超过了类对象;
3 、在静态成员方法中不允许使用非静态成员;
a. 不能直接使用非静态成员变量
b. 不能直接使用非静态成员方法
c. 不能直接使用 this 关键字
4 、static 修饰的静态成员方法一般用于封装成工具类。
static 修饰静态代码块
类文件加载时,一定会执行 static 修饰的静态代码块
1 、程序加载过程中,完成一些启动必须项 MySQL JDBC 加载驱动;
2 、可以在程序加载过程中,完成一些程序运行必须的数据 MySQL 连接数据库的必须数据。
6. 代码块
对类或对象进行初始化。
代码块可分为静态代码块和非静态代码块。(有无 static 修饰)
静态代码块:
用 static 修饰的代码块。
1 、可以有输出语句;
2 、可以对类的属性、类的声明进行初始化操作;
3 、不可以对非静态的属性初始化。即:不可以调用非静态的属性和方法;
4 、若有多个静态的代码块,那么按照从上到下的顺序依次执行;
5 、静态代码块的执行要先于非静态代码块;
6 、静态代码块随着类的加载而加载,而且值执行一次。
非静态代码块:
没有 static 修饰的代码块。
1 、可以用输出语句;
2 、可以对类的属性、类的声明进行初始化操作;
3 、除了调用非静态的结构外,还可以调用静态的变量和方法;
4 、若有多个非静态的代码块,那么按照从上到下的顺序依次执行;
5 、每次创建对象的时候,都会执行一次。且先于构造器执行。
7. 抽象类和抽象方法
7.1 什么是抽象类和抽象方法
用 abstract 关键字来修饰一个类,这个类叫做抽象类
用 abstract 关键字来修饰一个方法,该方法叫做抽象方法。
7.2 abstract 关键字
abstarct 关键字修饰的成员方法,是要求子类强制重写的。
1 、abstract 修饰的方法没有方法体;
2 、abstract 修饰的方法必须定义在 abstract 修饰的类内或者 interface 接口内 ;
3 、一个普通类继承于 abstract 修饰的类,需要完成实现在 abstract 类内的所有 abstract 方法;
4 、abstract 类没有自己的类对象。
8. interface
1 、用 interface 来定义 ;
2 、接口中的所有成员变量都默认是由 public static final 修饰的;
3 、接口中的所有抽象方法都默认由 public abstract 修饰的;
interface 关键字使用来声明新的 Java 接口,接口是方法的集合。
接口是 Java 语言的一项强大功能。
任何类都可以声明它实现一个或多个接口,这意味着它实现了在这些接口中所定义的所有方法。
实现了接口的任何类都必须提供在该接口中的所有方法的实现。
一个类可以实现多个接口。
9. 接口和抽象类的区别
相同点:
1 、都不能创建对象;
2 、都可以定义抽象方法,并且一定在子类中重写。
不同点:
1 、关键字不同 interface 和 abstract ;
2 、抽象方法中既可以有抽象方法也可以有普通方法;
3 、接口中所有的方法都是抽象方法;
4 、抽象类的方法可以任意权限,接口中的方法只能是 public ;
5 、抽象类只能单继承,接口可以多实现。
JDK 1.8 开始,接口中的方法不再是只能有抽象方法(普通方法会被隐式地指定为 public abstract 方法),它还可以有静态方法和 default 方法。并且静态方法与 default 方法可以有方法体!
让我们来看一下下面的例子:
public interface NewInterface {
static void staticMethod () {
System.out.println("staticMethod" );
}
default void defaultMethod () {
System.out.println("defaultMethod" );
}
public void getInfo () ;
}
由上面的例子可以看出给出一个接口,在 JDK 1.8 的环境下,他可以拥有静态方法和 default 方法,所谓 default 方法即是使用 default 关键字来修饰的方法。
一个接口可以有多个静态方法和 default 方法,没有个数限制。实现类只需要实现它的抽象方法即可。
关于静态方法和 default 方法的调用:
1 、对于静态方法,直接由接口名调用,不需要由接口实现类的对象来调用;
2 、对于 default 方法,很明显是需要实例对象来调用的。
import org.junit.Test;
public class NewInterfaceTest {
@Test
public void test () {
NewInterface.staticMethod();
new SimpleImpl().defaultMethod();
}
}
而且 default 方法的调用有一点需要特别注意。我们知道在 Java 中是单继承的,但是是可以实现多个接口的,所以,当一个类实现了多个接口之后,如果多个接口有着相同的 default 方法,即方法名和参数列表相同。那么此时就会出现问题,无法识别到底是调用的哪个接口的方法,这个时候就必须要在实现类里面显式重写 default 的方法,而关于 default 的方法的重写,我们在实现类中不需要继续出现 default 关键字也不能出现 default 关键字。
public class SimpleImpl implements NewInterface {
@Override
public void getInfo () {
System.out.println("INFO" );
defaultMethod();
}
public void defaultMethod () {
System.out.println("Impl default Method" );
}
}
重写的 default 方法必须的访问权限必须是 public ,因为 default 方法除了没有显式的访问修饰符外,只能用 public 访问限定符来修饰,而我们知道在 Java 中,要重写一个方法,访问限定符一定要大于或等于父类或者接口指定的访问限定符范围,而且方法声明处抛出的异常也要大于后者。所以访问权限必须是 public 。
最后,当 default 方法和实现类继承的父类的方法同名时,优先调用父类的方法
10. 内部类
在 Java 中,允许一个类定义位于另一个类的内部,前者称为内部类,后者称为外部类。
Inner class 的名字不能与包含它的外部类类名相同。
10.1 成员内部类(static 成员内部类和非 static 成员内部类)
class Outer {
private int s;
public class Inner {
public void mb () {
s = 100 ;
System.out.println("在内部类Inner中s=" + s);
}
}
public void ma () {
Inner i = new Inner();
i.mb();
}
}
public class InnerTest {
public static void main (String args[]) {
Outer o = new Outer();
o.ma();
}
}
10.2 局部内部类(不谈修饰符)
class 外部类 {
方法() {
class 局部内部类 {}
}
{
class 局部内部类 {}
}
}
1 、只能在声明它的方法或代码块中使用,而且是先声明后使用,除此之外的任何地方都不能使用该类。
2 、但是它的对象可以通过外部方法的返回值返回使用,返回值类型只能是局部内部类的父类或父接口类型。
3 、局部内部类可以使用外部方法的局部变量,但是必须是 final 的。
10.3 匿名内部类
匿名内部类不能定义任何静态成员、方法和类,只能创建匿名内部类的一个实例。
一个匿名内部类一定是在 new 后面,用其隐含实现一个接口或实现一个类。
匿名内部类的特点:
1 、匿名内部类必须继承父类或实现接口;
2 、匿名内部类只能有一个对象;
3 、匿名内部类对象只能使用多态形式引用。
interface A {
public abstract void fun1 () ;
}
public class Outer {
public static void main (String[] args) {
new Outer().callInner(new A() {
public void fun1 () {
System.out.println("implement for fun1" );
}
});
}
public void callInner (A a) {
a.fun1();
}
}
11. 异常处理
异常:
在 Java 语言中,将程序执行中发生"不正常情况称为" "异常" 。
(开发过程中的语法错误和逻辑错误不是异常)
Java 程序在执行过程中所发生的异常事件可分为两类:
1 、Error:
Java 虚拟机无法解决的严重问题。如:JVM 系统内部错误、资源耗尽等严重情况。一般不编写针对性的代码进行处理。
2 、Exception:其他因为编程错误或偶然的外在因素导致的一般性问题,可以使用针对性代码进行处理。
11.1 异常处理机制一:try-catch-finally
在编写程序时,经常要在可能出现错误的地方加上检测的代码,如进行 x/y 运算时,要检测分母为0 ,数据为空,输入的不是数据而是字符串等。过多的 if -else 分支会导致程序的代码加长、臃肿,可读性差。因此采用异常处理机制。
a.Java 程序的执行过程中如出现异常,会生成一个异常类对象,该异常对象将被提交给 Java 运行时系统,这个过程称为抛出(throw )异常。
b.如果一个方法内抛出异常,该异常对象会被抛给调用者方法中处理。如果异常没有在调用者方法中处理,它继续被抛给这个调用方法的上层方法。这个过程将一直继续下去,直到异常被处理。这一过程称为捕获(catch )异常。
c.程序员通常只能处理 Exception,而对 Error 无能为力。
异常处理是通过 try -catch -finally 语句实现的。
try {
} catch (ExceptionName1 e) {
} catch (ExceptionName2 e) {
}
[finally {
}]
如果抛出的异常是 IOException 等类型的非运行时异常,则必须捕获,否则编译错误。也就是说,我们必须处理"编译时异常" ,将异常进行捕捉,转化为"运行时异常" 。
11.2 异常处理机制二:throws
a.如果一个方法(中的语句执行时)可能生成某种异常,但是并不能确定如何处理这种异常,则此方法应显示地声明抛出异常,表明该方法将不对这些异常进行处理,而"由该方法的调用者负责处理" 。
b.在方法声明中用 throws 语句可以声明抛出异常的列表,throws 后面的异常类型可以是方法中产生的异常类型,也可以是它的父类。
c.重写方法不能抛出比被重写方法范围更大的异常。
12. 其他关键字
12.1 class
class 关键字用来声明新的 Java 类,该类是相关变量和 / 或方法的集合。
类是面向对象的程序设计方法的基本构造单位。
类通常代表某种实际实体,如几何形状或人。
类是对象的模版。
每个对象都是类的一个实例。
要使用类,通常使用 new 操作符将类的对象实例化,然后调用类的方法来访问类的功能。
12.2 new
new 关键字用于创建类的新实例。
new 关键字后面的参数必须是类名,并且类名的后面必须是一组构造方法参数(必须带括号)。
参数集合必须与类的构造方法的签名匹配。
= 左侧的变量的类型必须与要实例化的类或接口具有赋值兼容关系。
12.3 instanceof
instanceof 运算符的前一个操作符是一个引用变量,后一个操作数通常是一个类(可以是接口),用于判断前面的对象是否是后面的类,或者其子类、实现类的实例。如果是返回 true ,否则返回 false 。