面向对象编程OOP
- 抽象是有效控制程序复杂性的重要手段。类是目前支持抽象的最好工具
- 对象的特性是属性,对象的行为是方法
- 面向对象基本特点是抽象,封装,继承,多态
- 慎用继承,优先使用组合
所谓组合(Composition),就是在新类中创建现有类的对象。不管是继承和组合,都允许在新类中直接复用旧类的公有方法或字段。 ```java class Animal { public void beat(){
System.out.println("My heart is beating");
} public void breath(){
System.out.println("I'm breathing");
} } public class Cat { // 组合 private Animal animal; // 使用构造函数初始化成员变量 public Cat(Animal animal){
this.animal = animal;
} // 通过调用成员变量的固有方法使新类具有相同的功能 public void breath(){
animal.breath();
} // 通过调用成员变量的固有方法使新类具有相同的功能 public void beat(){
animal.beat();
} // 为新类增加新的方法 public void run(){
System.out.println("I'm running");
}
public static void main(String[] args) {
// 显式创建被组合的对象实例 animal Animal animal = new Animal();
// 以 animal 为基础组合出新对象实例 cat
Cat cat = new Cat(animal);
// 新对象实例 cat 可以 breath()
cat.breath();
// 新对象实例 cat 可以 beat()
cat.beat();
// 新对象实例 cat 可以 run()
cat.run();
} }
//继承
public class Cat extends Animal{
// 为新类增加新的方法
public void run(){
System.out.println(“I’m running”);
}
}
Cat cat = new Cat();
// 子类实例 cat 可以 breath()
cat.breath();
// 子类实例 cat 可以 beat()
cat.beat();
// 子类实例 cat 可以 run()
cat.run();
以上便是组合实现复用的方式,Cat 对象由 Animal 对象组合而成,如上面的示例代码,在创建 Cat 对象之前先创建 Animal 对象,并利用这个 Animal 对象来创建 Cat 对象。<br />实际上,组合表示出来的是一种明确的**整体-部分**的关系。而对于继承来说,是将某一个抽象的类,改造成能够适用于不同特定需求的类。
---
- 以上看起来组合比继承麻烦,但是**继承会打破封装性**,违反了 OOP 原则。迫使开发者去了解父类的实现细节,子类和父类耦合 而且**父类更新后可能会导致一些不可知的错误**
<br />
<a name="uTKMQ"></a>
## 面向过程编程POP
面向对象相比于面向过程对于方法和属性运用的效率更高。
---
<a name="FokvY"></a>
# 类和对象
- **java 程序由类和对象构成,而类又由方法和变量构成。java 的方法由语句构成,而语句又由标识符和运算符构成**
**类概括了同类对象的共有性质:数据和方法。**
- **类体中不能有独立的执行代码,所有的执行代码只能出现在方法中。**
- **类的对象又称为类的实例**
<a name="if7JF"></a>
# 引用
- **只有引用数据类型才有引用的概念,所以如string变量可以称为对象变量**
- **在 Java 中,任何对象变量的值都是对存储在另外一个地方的一个对象的引用**
- **在 Java 中,一切都被视为对象,但操纵的标识符实际上是对象的一个引用(reference)**。
举个形象点的例子:我们可以用遥控器(引用)去操纵电视(对象)。只要拥有对象的“引用”,就可以操纵该“对象”。换句话说,我们无需直接接触电视,就可通过遥控器(引用)自由地控制电视(对象)的频道和音量。此外,没有电视,遥控器也可以单独存在。就是说,你仅仅有一个“引用”并不意味着你必然有一个与之关联的“对象”。
- 如创建一个String引用`String s;`此时对 s 应用 String 方法,会报错。因为此时 s 没有与任何对象进行关联。**只有对象及其引用才能调用成员方法**
- **创建一个引用的同时进行初始化即可关联对象 如**`String s = "小牛肉";`
- **将对象变量设置为null即表示没有****引用任何对象,****所以null引用也无法调用方法**
- **如果2个变量指向的是同一个对象。那么修改任何一个引用变量的值都会影响另外一个变量**
```java
Person{
int age;
main{
person p1.age=10;
p2=p1;
p2.age=8;
sout(p1.age); ->p1.age=8;
}
}
New
- new关键字是最常用的创建对象的方式,它返回的是一个引用
- 如
String s=new String("小牛肉")
是创建了一个String类型的对象,并将该对象的引用存储到对象变量s中
- 如
- 还可以让对象变量引用一个已经存在的对象
String str = new String("小牛肉"); String s; s = str
这是让对象变量s
引用str引用的对象
clone()
借助Object.clone()方法可以复制一个对象的复制。它们的各种属性都一样,但是之后可以改变各自属性而互不影响,详见链接
变量
变量按照定义域分为局部变量和全局变量。按照类型分为基本类型变量和引用类型变量。
引用类型变量可以定义为null,基本类型不可以
- 局部变量:方法体内和参数列表中的变量,位于栈内存
- 成员变量:方法体外的变量,又分为静态变量(类变量)和成员变量(成员属性)。位于堆内存
- 如果类的成员变量(字段)是基本类型,那么在类初始化时,这些类型将会被赋予一个初始值。这样可以减少bug的来源。(特别是创建构造函数时)
- 不管是局部还是成员,最好都显式的主动初始化 | 基本类型 | 初始值 | | —- | —- | | boolean | false | | char | \u0000 (null) | | byte | (byte) 0 | | short | (short) 0 | | int | 0 | | long | 0L | | float | 0.0f | | double | 0.0d |
作用域
-
变量的作用域
变量的作用域都是由{}的位置决定!即只限于{}内
- 类中的其他方法都能访问成员变量,但是主函数不能,这不是作用域的原因,这是由于“静态的只能访问静态的”
见第一段代码
- 类中的其他方法都能访问成员变量,但是主函数不能,这不是作用域的原因,这是由于“静态的只能访问静态的”
成员变量在方法内会被同名局部变量覆盖(隐藏)
- 见下,不能在2个嵌套的块中声明同名的变量
见第二段代码
main{
int x = 12;
{
int q = 96;
}
sout(x+q); //此时会报错,因为识别不到q这个变量。
//q作用域只限于第二个{}中
//x限于第一个花括号,而第一个{}包含了第二个{} 所以x第二个花括号内也能访问到
}
{
int x = 12;
{
int x = 123; // c语言、c++中合法,java中非法。可以看成是在12x看来只有一个叫x的变量,但是在123x看来,有2个叫x的变量,重复了
}
}
对象的作用域
- Java 对象不具备和基本类型一样的生命周期。当用 new 创建一个 Java 对象时,它可以存活于作用域之外。但是它的引用作用域还是看{}位置
Java 有一个垃圾回收器,用来监视 new 创建的所有对象,并辨别那些不会被再引用的对象,然后释放这些对象的内存空间
public class Test { public static void main(String[] args) { String s = new String("DDD"); } Test() { System.out.println(s); //此时报错,访问不到s } }
调用顺序
-
方法
分为实例方法和类方法。实例方法又叫成员方法。类方法又叫静态方法
方法名和参数列表统称为方法签名(signature of the method)。签名是作为方法的唯一标识。
按值调用 (call by value ) 表示方法接收的是调用者提供的值。
- 按引用调用 ( call by reference ) 表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值
- Java 程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,方法不能修改传递给它的任何参数变量的内容。(只限于基本类型) ```java public static void tripleValue(double x){ x *= 3; }
double percent = 10; tripleValue(percent);//调用这个方法之后,percent 的值还是 10。下面看一下具体的执行过程: / x 被初始化为 percent 值的一个拷贝(也就 是 10 ) x 被乘以 3 后等于 30。 但是 percent 仍然是 10 这个方法结束之后,参数变量 x 不再使用。 /
- **对象引用作为参数就不同了**,可以很容易地修改对象的字段值
```java
public class Employee { // 员工类
private int salary; //薪水
Employee(int salary){
this.salary=salary;
}
public static void tripleSalary (Employee x) { //3倍的工资
x.salary*=3;
}
public static void main(String[] args) {
Employee harry = new Employee(200); //哈利的工资是200
tripleSalary(harry); //这里传入对象作为参数,方法便轻易的修改了方法的字段值
System.out.println(harry.salary);
}
}
可变参数
- 在 「JDK 1.5」 之后,如果我们定义一个方法需要接受多个参数,并且「多个参数类型一致」,我们可以对其简化成如下格式:
修饰符 返回值类型 方法名 (参数类型... 形参名){ }
… 用在参数上,称之为「可变参数」,它「表明这个方法可以接收任意数量的参数」
等价于
修饰符 返回值类型 方法名 (参数类型[] 形参名){ }
- 一个方法里只能有一个可变参数,且可变参数得是最后一个参数位置
```java
public class ChangeArgs {
//可变参数写法
public static int getSum(int… arr) {
int sum = 0;
for (int a : arr) {
} return sum; }sum += a;
public static void main(String[] args) { int[] arr = { 1, 4, 62, 431, 2 }; int sum = getSum(arr); System.out.println(sum);
int sum2 = getSum(6, 7, 2, 12, 2121); System.out.println(sum2); } } ```
static
- 一个方法里只能有一个可变参数,且可变参数得是最后一个参数位置
```java
public class ChangeArgs {
//可变参数写法
public static int getSum(int… arr) {
int sum = 0;
for (int a : arr) {
- 静态的只能访问静态的,而实例的可以访问实例也可访问静态的。
- 静态要访问实例必须借助对象
- static只能修饰成员变量,方法,内部类,代码块(不能修饰局部变量)
- 类变量的内存只有一处,让类的所有对象共享它属于整个类,而不属于类的某个对象。
Test类有静态字段int a=5; 创建t1,t2两个对象。此时它们的a都为5。如果修改t1.a=8; 则t2.a也变为8
- 引用类变量途径有两条,或通过类,或通过对象;当在其所在类调用时,可以省略类直接调用
- 因为静态区属于类,所以不应该通过对象进行调用,虽然可以执行,但是会警告
- 类方法调用和类变量调用方法差不多
- 一个方法如果与它所在类的实例对象无关,那么它就应该设置为静态方法
- static代码块:静态代码块会在jvm加载类的时候首先被执行,且从始至终只会被执行一次。即执行顺序优先于main方法
- 非静态代码块在new对象时都会被执行一次
-
访问权限
java所有变量和方法和类默认都是default。default不能自己定义,只能默认为省略状态。
- 没有修饰的成员变量和方法称为友好变量和友好方法。即default。
- 访问权限不能修饰局部变量,局部变量访问范围只在方法体中
- 访问权限有四个 private<default<protected<public 控制级别由小到大
- private:只能被当前类的其他成员访问
- default:只能被同一包的其他类访问
- protected:作用于继承关系。可以被子类访问,以及子类的子类
- public:随便访问,其他包都可以来访问
final
final意思为最终的,可以修饰类,方法,变量
- 修饰类:final不能被继承
- 修饰方法:final方法不能被重写(最终的方法)
- 修饰变量:final变量不能被改变值,即最终的变量 这种用法一般用于创建常量
private只能修饰内部类。private属性和方法不能被继承(无法访问到也就不能继承)
this
常常用于封装
用法:
- 调用被局部变量覆盖的成员变量
- 调用所在类的成员方法,这种用法可以省略this
- 调用构造方法(构造函数里调用本类的其他重载构造函数) 此种用法时this语句得是构造方法里第一条语句。但是只允许调用一次构造函数
- 作为一个对象使用
- 注意作用域问题和静态实例调用问题,这些使得this只能用于成员方法中
- this本质是当前对象的引用(当前对象即调用this方法的那个对象),所以1,2,3相当于当前对象调用成员变量,成员方法……
如public boolean equals(Object obj) { return (this == obj); }
对象a.equals(对象b) 即返回a==obj
super
- super用于调用父类的成员(成员变量和方法)。常常用于调用被覆盖的父类成员变量或成员方法
子类构造方法在实例化对象时会隐式调用父类无参构造方法(
**即super();**
) 如果父类定义了有参构造,则子类隐式调用无参会失败。这时要么子类手动写有参super(),要么父类增加无参构造super() //访问父类无参构造方法 super(?,?,...) //访问父类有参构造方法 super.?/?() //访问父类属性/方法
return
return 语句返回值后会退出整个方法。所以其他语句要写return之前;
-
构造方法
构造方法的名与它的类名相同,并且不返回结果,也不写上 void 关键字。构造方法的作用是创建类的对象,并给对象初始化。构造方法是公共方法,但程序不能显式调用构造方法。(即只能通过new告诉编译器调用构造方法)程序运行时,当有对象要创建时,由系统自动调用构造方法。
如果类的声明没有定义构造方法,系统就增补一个没有参数的默认构造方法。
- 如果写了一个有参构造,那么系统就不会再增补无参构造。自己记得根据情况写一个无参构造
对象
类被声明后,就可用类创建对象,被创建的对象称为类的实例。程序使用对象需依次经历4个步骤:声明对象、创
建对象、使用对象和撤销对象。
创建对象即对对象进行初始化,即分配内存,又叫实例化内存
抽象类&接口
抽象类
- 抽象类只声明一种模板,不应该有具体实现代码的类。只有它的子类才能是有实际意义的类,接口也一样
- 但是可以创建抽象类的引用指向子类对象
**abstract class T{}**
抽象类中一定得有抽象方法,有抽象方法的类一定是抽象类(抽象方法只能出现在抽象类),也可以有普通方法
抽象类不能有实例
抽象方法
不能有具体实现语句,即只能声明方法不能实现方法
如**abstract void dd();**
这便是抽象方法
接口
- 接口是纯粹的抽象类,只能有抽象方法不能有普通方法
interface T{}
- 接口可以继承其他接口,但是不能被类继承
- 接口名通常以 able 或 ible 结尾,意指能做什么
- 接口 的所有变量都默认为
**public final static **
属性;所有的方法都默认为是**public abstract **
属性- 因为是final,因此不能被修改且必须有初始值
- 一个类实现接口必须实现所有抽象方法(相当于实现所有接口方法)
- 在 Java 8 中,允许在接口中增加静态方法和默认方法
- 接口中默认方法使用default,可以有方法体。实现类不强制重写父接口的default
- 实现类无法通过super访问父接口的defult方法
在实现接口的方法时,方法的名字、返回值类型、参数个数及类型必须与接口中的定义的方法完全一致
重载Overload
- 即方法重载。方法重载只需要方法名相同,参数列表和返回值,修饰符可以不同(因此static,final,普通都可以进行重载)
参数名不能作为编译器区分重载方法的依据。因为jvm是根据方法签名来区分方法的
- 如果调用重载方法,不存在相同参数类型的重载方法,则实参自动类型转换升一级再寻找方法进行调用
- 如果传入参数大于所有重载方法的参数类型,就要强制转换类型,否则报错
主函数main也是可以重载的,但是参数非String数组的main只能人为调用
class PrimitiveOverloading { void f2(float x) { System.out.println("f2(float)"); } void f2(double x) { System.out.println("f3(double)"); } } public class demo { public static void main(String[] args) { PrimitiveOverloading p = new PrimitiveOverloading(); p.f2(5); } } //输出:f2(float)
继承
子类又叫派生类。父类又叫超类。
java可以多层继承,但是不可以多重继承。但是可以实现多个接口,也就相当于多重继承**java.lang.Object**
是所有类的超类
子类只会继承父类非私有的成员作为自己的成员
多层继承
多层继承中构造方法的调用顺序与类的调用顺序一致:从最高层次类调用至最低层次类
如class T3 extends T2;T3(){sout T3}
class T2 extends T1;T2(){sout T2}
class T1;//最高级别类 T1(){sout T1}
主函数创建T3对象,会先执行T3()
子类构造函数在实例化对象时默认调用父类无参构造方法
然后T3()调用T2() T2()调用T1()
最后输出T1 T2 T3
重写Override
- 子类如果创建一个与继承方法同名的方法。该方法会覆盖继承的父类方法。
- 重写父类与子类的方法必须方法签名一致,返回值也相同
- jdk1.5后允许重写的子类方法返回值与父类不同,但是返回值类型必须是父类返回类型的子类,如父类返回值类型为Object,子类返回值类型为String
- 子类重写父类方法时,不能使用比父类中被重写方法更严格的访问权限
final方法不能被重写,static也不能。因为final是最终的不可修改,而sttatic是独属于一个类的,像构造函数一样,因此也不能被重写
封装
成员变量设置为私有,通过公有方法获取获取成员变量。这样加强了数据的安全性
多态Override
多态即使得同一个行为具有多个不同表现形式或形态的能力,具体的体现即:Override重写
如Father f=new Son();
父类对象的引用指向子类对象,即用起来本质还是子类对象,但是理解起来理解为父类就行 这里的父类包括父接口,父抽象类等等
可以详见LinkedList之上下转型应用多态子类方法会覆盖父类同名方法。但是子类的属性不会覆盖父类的同名属性。不过子类方法获取同名属性还是获取的子类的属性
public class Main2 extends Main { int a=2; public int getA(){ return a; } public static void main(String[] args) { Main main=new Main2(); System.out.println(main.a); //1 System.out.println(main.getA()); //2 } } class Main { int a=1; int getA(){ return a; } }
上/下转型
即父子类对象的类型转换
例如Animals类和Dog类,animal和dog2个对象
上转型(即向上转):即引用是父类类型 上转型可能会丢失子类的一些方法Animals animals2 =(Animals)dog;
//上转型可以省略强转Animals animals3=new Dog();
上转型即多态//省略了(Animal) 这种方法不能用于下转行,下转型只能转变经历了上转型的对象,且不能省略强转
下转型(即向下转)Dog dog2=(Dog)animals2;
- 上下转型可以这样理解:子类继承父类,又可以增加自己的东西,所以父类相当于子类的一部分,所以子类能直接转为父类,因为父类需要的东西子类都有。但是父类转子类,不一定有子类的东西,所以转不了。(不考虑父类有私有的情况)
如果一个类实现与继承的类不止一个(实现+继承的数量不止一个),那么如果它的对象是A接口/类的多态对象,那么它要调用B类的接口或者对象,就需要下转型,这样才能既调用A方法也调用B方法。因为多态对象只能调用父接口或者父类所拥有的方法(父接口与父类也实现。继承了类/接口,也是一个道理)
public class Xiaoming implements Ba,Person{ public String ret(){ return "我是小明"; } @Override public String ret2() { return "Ws"; } public static void main(String[] args) { Person person=new Xiaoming(); System.out.println( ((Xiaoming) person).ret2()); } }
联编(又称为绑定)
编译时暂不绑定调用哪个方法,必须在运行时才绑定调用方法的技术称为动态联编(重写)。
而重载是静态联编,编译时根据参数列表进行绑定
联编是将发送给对象的消息与含执行该消息方法的对象连接起来。代码块
除了普通代码块是伴随着方法写。其他代码块都是直接写在类里
- 静态代码块 > 构造代码块 > 构造函数
- 普通代码块:即方法定义时,方法体的{}
- 静态代码块:静态代码块会在jvm加载类的时候首先被执行,且从始至终只会被执行一次。即执行顺序优先于main方法
- 同步代码块: 使用 synchronized 关键字修饰,并使用“{}”括起来的代码片段,它表示同一时间只能有一个线程进入到该方法块中,是一种多线程保护机制。
构造代码块:即没有任何修饰的{}块。构造代码块和构造函数一样同样是在生成一个对象时被调用。执行时会把构造代码块里的语句插入到所有构造函数的最前端
public class Test { /** * 构造代码 */ { System.out.println("执行构造代码块..."); } /** * 无参构造函数 */ public Test(){ System.out.println("执行无参构造函数..."); } /** * 有参构造函数 * @param id id */ public Test(String id){ System.out.println("执行有参构造函数..."); } } 插入后相当于 public Test(){ System.out.println("执行构造代码块..."); System.out.println("执行无参构造函数..."); } /** * 有参构造函数 * @param id id */ public Test(String id){ System.out.println("执行构造代码块..."); System.out.println("执行有参构造函数..."); }
对象拷贝
浅拷贝:浅拷贝会在堆上创建一个新的对象,不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝的属性和原对象的属性共用同一个内部对象。
- 深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
- 深拷贝就是拷贝完对象后,再让子属性再调用clone获得新对象,然后set进父对象里
- 拷贝方法是
clone()
,位于Object
类,实现了对象中各个属性的复制,但它的可见范围是protected
的(最大到保护级别,不能访问public)- 实体类要使用拷贝,需要实现Cloneable接口,这是一个标记接口,自身没有方法。并覆盖clone()方法,将可见性提升为public。
- 引用拷贝就是直接完全相等,无论是该对象还是对象的属性。如直接赋值对象
Person person1Copy=person1;
```java public class Address implements Cloneable{ private String name; // 省略构造函数、Getter&Setter方法 @Override public Address clone() { try {
} catch (CloneNotSupportedException e) {return (Address) super.clone();
} } }throw new AssertionError();
public class Person implements Cloneable { private Address address; // 省略构造函数、Getter&Setter方法 @Override public Person clone() { try { Person person = (Person) super.clone(); return person; } catch (CloneNotSupportedException e) { throw new AssertionError(); } } }
//——————测试—————————————— Person person1 = new Person(new Address(“武汉”)); Person person1Copy = person1.clone(); //拷贝person1 // true,内部的引用类型的属性与原对象的内部的该属性是相同 System.out.println(person1.getAddress() == person1Copy.getAddress());
```java
@Override
public Person clone() {
try {
Person person = (Person) super.clone();
person.setAddress(person.getAddress().clone());
return person;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
//------------测试-------------------------
Person person1 = new Person(new Address("武汉"));
Person person1Copy = person1.clone();
// false
System.out.println(person1.getAddress() == person1Copy.getAddress());
hashCode()
- hashCode() 的作用是获取哈希码(int 整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。位于
Object
,java里的hashCode用来将对象的内存地址转换为整数之后返回,可用于判断2个对象是否相等 - hashCode在一些容器(比如 HashMap、HashSet)中,有了 hashCode() 之后,判断元素是否在对应容器中的效率会更高。
- 如HashSet添加元素,首先通过hashCode判断。如果存在同样的 hashCode的对象,再继续使用 equals() 来判断是否真的相同。比直接用equals()更快
- 即同样的hashCode的对象不一定相等,但是不相同的hashCode一定不同
- 重写 equals() 时必须也重写 hashCode() 方法
- hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。