OOP 面向对象编程
面向对象编程(Object Oriented Programming,OOP,面向对象程序设计)是一种计算机编程架构。强调的是具有某些特定功能的对象。
面向过程编程(Procedure Oriented Programming,POP)是一种以过程为中心的编程思想。强调的是功能行为,整个过程的实现。
OOP达到了软件工程的三个主要目标:重用性、灵活性和扩展性。
OOP 的一条基本原则是计算机程序是由单个能够起到子程序作用的单元或对象组合而成。
所以这里对象在OOP中是至关重要的角色。那么什么是对象?
对象,一般指类在内存中装载的实例,具有自己的成员变量和成员函数(方法)。
可以理解为一件实实在在的物品,它具有它特有的属性以及自己的功能。
这个实实在在的物品便是对象。把这种具有它的属性及其功能的物品抽象出来描述一种事物,便称之为类。因此,类就是一种定义的具有成员变量和成员函数的抽象数据类型。
比如:汽车就可以定义为一个类。它自身具有形状,大小,颜色等属性,它能移动,加速,减速,刹车等行为。
Class Car{
String shape;// 形状属性 --成员变量
int size; // 大小属性
String color;// 颜色属性
void move() {
// 移动行为 --成员函数(方法)
}
void stop() {
// 刹车行为
}
}
类的定义包括“成员变量”的定义和“方法”的定义:
Class 类名 {
// 成员变量定义
成员变量类型 变量名称;
// 方法定义
修饰词 返回值类型 方法名称([参数列表]) {
方法体...
}
}
类定义完成后,对象的创建可通过new关键字创建,创建对象的过程通常被称为实例化。
new 类名();
eg: new Car(); // 创建一个汽车的对象。 = 你生产了一辆汽车
为了能够对实例化对象进行访问控制,需要用一个特殊的变量---引用
--引用类型变量可以存放该类对象的地址信息,通常称为“指向该类的对象”。当一个引用类型变量指向该类的对象时,就可以通过这个变量对对象实施访问。
--除8种基本类型之外,用类、接口、数组等声明的变量都称为引用类型变量,简称“引用”。
Car car = new Car();
Car -- 类
car -- 引用 存放对象的地址。指向new Car() 对象
new Car() -- 对象
通过引用就可以访问对象的成员变量和调用方法。
Car car = new Car();
car.size = 100;
car.color = "red";
car.move();
注意:不同的引用指向同一个对象时,操作对象时,都会改变。
Car car1 = new Car();
Car car2 = car1;
car1.color = "white";
car2.color = "black";
System.out.println(car1.color); // 这里会输出black,因为car1和car2指向同一个对象。
方法重载(overload):方法名相同,参数列表不同(参数数量、参数类型)。
使用时编译器在编译时会根据方法签名(方法签名:方法名和参数列表)来绑定调用不同的方法。通过传入不同的参数个数或参数类型来调用不同的方法。使得代码更加灵活,简洁。
构造方法:方法名与类名相同;没有返回值,但是也不能写void。构造方法用于实现对象的初始化。任何一个类都必须包含构造方法;当未自定义构造方法时,编译器在编译时会添加一个无参的空构造方法(默认的构造方法);当定义了构造方法时,编译器将不再添加默认的构造方法。
Class Car {
int size;
String color;
public Car (int size) {
this.size = size; // this关键字在方法体中 表示调用该方法的当前对象。哪个对象调用该方法,this就是哪个对象。
}
}
Class TestCar {
public static void main(String args[]) {
Car car = new Car(20); // 新建一个对象Car,并初始化size为20
}
}
OOP面向对象的几大特性:继承、封装、多态。(下面没有具体分类,他们相互联系,相互依赖)
继承,正如其名就是继承上一代的东西。继承某对象的属性和方法,并且自己还可以扩展自己的属性和方法。通常具有继承关系的类被称为子类和父类。子类继承自父类。一般通用的属性和方法都放在父类,子类继承后也有这些属性和方法,并可以添加自己的属性和方法。一个类只能继承一个父类。Object是最上级的父类。如果未写继承关系,默认继承自Object类。所以默认的类都有equals、hashCode、notify方法
class A {
int a = 1; // A对象属性a
public void sum(int a1, int a2) { // A对象方法sum求和
a = a1 + a2;
}
}
class B extends A { // B继承自A,这时B对象拥有A的属性和方法
int b = 2;
public void multiply (int b1, int b2) { // B对象的拓展方法multiply求积
b = a * b1 * b2;
}
}
public class Test {
public static void main(String args[]) {
B b = new B();
b.sum(1, 2);
System.out.println(b.a); // 结果为3
b.multiply(4, 5);
System.out.println(b.b); // 结果为60,先执行了1+2,再执行了3*4*5
}
}
继承不仅让代码变得简洁,还增加了代码的可重用性,拓展,修改。这样子类只负责子类的东西,不用管父类的东西,父类只要做好自己的工作便是。
比如教师,辅导员公共的部分可作为父类教职工,教师,辅导员都作为子类。子类只需要关注自己的相关行为和状态,无须关注父类的行为和状态。他们们共有的姓名,性别,年龄属性都交给父类教职工管理。
注意:子类的构造方法必须通过super关键字调用父类的构造方法(super关键字可理解为父类对象)。如果子类的构造方法没有调用父类的构造方法,编译器会自动加入对父类无参构造的调用。(所以有继承关系时,父类必须要加上无参构造方法,防止子类未加入对父类构造方法的调用)
向上造型:一个子类的对象可以向上造型为父类的类型。即定义父类型的引用可以指向子类的对象。但是通过父类的引用只能访问父类所定义的成员,不能访问子类扩展的部分。通常看引用后“.”出来的属性方法就是看该类型具有哪些属性方法。
class Parent {
int pp;
public void pm() {...}
}
class Child extends Parent {
int cc;
public void cm() {...}
}
Parent parent = new Child();
parent.pp = 100;
parent.pm();
parent.cc = 200; // 编译出错
parent.cm(); // 编译出错
方法重写(override):子类可以重写(覆盖)继承自父类的方法,即方法签名一样,但是方法实现不同。当子类对象的重写方法被调用时(无论是通过子类的引用还是父类的引用 调用),运行的都是子类重写后的方法。
class Parent {
public void mm() {
System.out.println("parent");
}
}
class Child extends Parent {
public void mm() {
System.out.println("child");
}
}
Parent pp = new Child();
pp.mm(); // 输出child
Child cc = new Child();
cc.mm(); // 输出child
这里pp和cc两个引用都是指向了Child对象,并且mm()被child重写,所以pp.mm()实际是执行的child对象的方法。方法实现看对象
重载和重写的区别:
--重载是指在一个类中定义多个方法名相同参数列表不同的方法。在编译时,根据参数个数和类型来决定绑定哪个方法。
--重写是指在子类中定义和父类完全相同的方法。在运行时,根据对象的类型不同(不是引用类型)来调用不同的版本。
class Parent {
public void mm() {
System.out.println("parent.mm()");
}
}
class Child extends Parent {
public void mm() {
System.out.println("child.mm()");
}
}
class Goo {
public void gg(Parent obj) {
System.out.println("gg(Parent)");
obj.mm();
}
public void gg(Child obj) {
System.out.println("gg(Child)");
obj.mm();
}
}
public class Test {
public static void main(String args[]) {
Parent obj = new Child();
Goo goo = new Goo();
goo.gg(obj); // 输出结果 gg(Parent) child.mm()
// 分析:
// 重载遵循所谓"编译期绑定",在编译时根据参数的类型判断调用哪个方法;因为obj的类型是Parent,所以Goo的gg(Parent obj)方法被调用。
// 重写遵循所谓"运行时绑定",在运行时根据引用变量指向的实际对象类型调用方法;因为obj指向对象Child,所以Child重写后的mm方法被调用。
}
}
封装,将一个功能的实现封装成对外提供可调用的、稳定的功能。封装可以使类具有独立性和隔离性,保证类的高内聚;避免使用非法数据赋值,降低代码出错的可能性,便于维护;避免类内部实现发生改变时,导致整个程序的改变。
根据封装类的实现依赖类的修饰符(public、protected、默认、private、)来控制封装类的使用权限。
访问控制符修饰成员时的访问权限如下:
修饰符 | 本类 | 同一个包中的类 | 子类 | 其他类 |
public | 可以访问 | 可以访问 | 可以访问 | 可以访问 |
protected | 可以访问 | 可以访问 | 可以访问 | 不能访问 |
默认 | 可以访问 | 可以访问 | 不能访问 | 不能访问 |
private | 可以访问 | 不能访问 | 不能访问 | 不能访问 |
static关键字:被static关键字修饰的变量或者方法不需要依赖于对象来进行访问,只要类被加载了,就可以通过类名来进行访问。static可以修饰类的成员变量、成员方法,另外还可以编写static代码块来优化程序性能。
注意:静态成员变量虽然独立于对象,但是不代表不可以通过对象去访问,所有的静态方法和静态变量都可以通过对象访问(只要访问权限足够)。
1、static修饰成员变量:static变量也称为静态变量,静态变量和非静态变量的区别:静态变量被所有对象所共享,在内存中只有一个副本,只有当类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。static成员变量和类的信息一起存储在方法区,而不是在堆中,一个类的static成员变量只有“一份”。操作该变量,会影响到其他对象的访问。
class Cat {
private int age;
private static int num;
public Cat(int age) {
this.age = age;
System.out.println(++num);
}
}
public class Test {
public static void main(String args[]) {
Cat c1 = new Cat(2); // 输出1
Cat c2 = new Cat(3); // 输出2
}
}
2、static修饰方法:static方法一般被称为静态方法,由于静态方法不依赖于任何对象就可以进行访问,因此对静态方法来说,是没有this的。所以在静态方法中不能访问非静态成员变量和非成员方法,因为非静态成员变量/方法都必须依赖于具体的对象。static方法的作用一般就用于提供一些“工具方法”和“工厂方法”。最常见的static方法就是main方法,因为执行main方法时没有创建任何对象,所以只有通过类名来访问。eg:Math.sin(); Arrays.sort();
3、static块:属于类的代码块,在类加载期间执行的代码块,只执行一次,一般用于加载静态资源或者加载一些配置等。
final关键字:final关键字用于修饰类,方法,变量,表示不可变。
final修饰变量,变量的值或者引用的对象不能改变。修饰成员变量必须初始化:声明时初始化或构造函数中初始化。也可以修饰局部变量,在使用之前初始化即可。
final修饰方法,子类不能重写该方法。作用用于锁定方法,防止任何继承他的类修改内部实现。类的private方法会被隐式地指定为final方法。
final修饰类,该类不能被继承。final类中的成员变量可根据需要自行设定为final变量,final类中的成员方法会被隐式地指定为final方法。常见的final类:String类、Math类...
static final修饰的成员变量被称为常量,必须声明时同时初始化,不可被修改。通常常量命名都是大写。常量在编译期会被替换。
class Foo {
public static final double PI= 3.14;
}
class Circle {
double area(double r) {
return r * Foo.PI; // 代码编译时,会替换成 return r * 3.14;
}
}
abstract关键字:abstract抽象,修饰类和和方法,被称为抽象类和抽象方法。
一个类中如果包含抽象方法,该类也必须声明为抽象类;如果一个类继承了抽象类,必须重写其抽象方法或者该类也声明为抽象类;abstract和final关键字不可以同时修饰一个类,因为final类不可继承,abstract类如果不继承就没有意义。
抽象类的意义:为子类提供公共的类型;封装子类中重复内容(成员变量和方法);提供统一方法定义,但是子类可以有不同的实现。
接口:接口用interface关键字声明。接口可以看作是特殊的抽象类。只包含抽象方法,不能定义成员变量,但是可以定义常量。eg:interface Runner {}
接口实现:实现用implements关键字实现一个接口。一个类可以实现多个接口,之间用逗号隔开。必须实现这些接口的方法。
接口继承:和类继承一样,子接口继承了父接口中定义的方法。
抽象类和接口:抽象类可以抽象公共的属性和方法,作为父类的存在,它定义的抽象方法也可以被子类有不同的实现。接口着重于方法定义,实现接口就有不同的行为。接口可以实现多个,抽象类只能继承一个。
多态,多态是在继承的基础上实现的。多态的三要素:继承、重写和父类引用指向子类对象。父类引用指向不同子类对象时,调用相同的方法,呈现出不同的行为就是类多态特性。多态可以分为编译时多态和运行时多态。
多态的意义:一个类型的引用在指向不同的对象时会有不同的实现;当然同样一个对象,造型成不同的类型时,也会有不同的功能。
向上造型:一个类的对象可以向上造型的类型有:父类的类型;其实现的接口类型。
强制转型:可以通过强制转换将父类型变量转换为子类型变量,前提是该变量指向的对象确实是该子类类型;也可以通过强制转换将变量转换为某接口类型,前提是该变量指向的对象确实实现了该接口。如果强制转换违背了这两个前提,将抛出异常ClassCastException。
instanceof关键字:为了避免ClassCastException,可以通过instanceof关键字判读某个引用指向的对象是否可以强制为某类型。
内部类:一个类定义在另一个类的内部,这个类被称为内部类Inner,所在的类被称为Outer。内部类只服务于Outer,对外不可见,内部类可以直接调用Outer的成员和方法。内部类对象的创建会有一个隐式的引用指向创建它的Outer类对象。
class Out {
private int num;
Out (int num) {
this.num = num;
In in = new In();
in.numInc();
}
public void printNum() {
System.out.println(num);
}
class In { // 内部类
void numInc () {
num++;
}
}
}
Out out = new Out(100);
out.printNum(); // 输出101 In对象创建时有个隐式的引用指向Out对象,调用numInc()会对Out的num属性进行操作。
匿名内部类:如果我们需要一个类的对象(通常这个类需要继承某个类或实现某个接口),而且对象创建后,这个类的价值就不存在了,那么这个类就不用命名,被称为匿名内部类。
interface Action {
public void execute();
}
class Act {
Action action = new Action() {
//TODO 这里可以定义成员变量和方法
@Override
public void execute() {
System.out.println("action");
}
};
Act() {
action.execute();
}
}
Act act = new Act();