文章目录
面向对象编程(OOP)
面向过程编程特点
- 是把模型分解成一步一步的过程。
- 线性思维(第一步,第二步)
面向对象编程特点
-
是一种通过对象的方式,把现实世界映射到计算机模型的一种编程方法。
-
分类的思维模式
-
再对分类下的细节进行面向过程
-
以类的方式组织代码,以对象的形式组织(封装)数据
面向对象基础
类 class
-
class是一种对象模版,它定义了如何创建实例,它本身就是一种数据类型
-
instance是对象实例,instance是根据class创建的实例,可以创建多个instance,每个instance类型相同,但各自属性可能不相同
-
class与instance就像人类与小明一样的关系(类与对象)
-
一个
class
可以包含多个字段(field
),字段用来描述一个类的特征class Person { public String name; public int age; }//两个字段
-
通过class,把一组数据汇集到一个对象上,实现了数据封装
-
一个Java源文件可以包含多个类的定义,但只能定义一个public类,且public类名必须与文件名一致。如果要定义多个public类,必须拆到多个Java源文件中。
private
-
public
是用来修饰字段的,它表示这个字段可以被外部访问,但是直接把field
用public
暴露给外部可能会破坏封装性(例如int age=-12),容易造成逻辑混乱,我们可以用private
修饰field
,拒绝外部访问class Person { private String name; private int age; }//此时再对name进行赋值将会报错,因为private拒绝外部访问,若想要修改必须使用方法(写在类内部)
实例 instance
-
根据对象模版创建出真正的对象实例,必须用new操作符
Person ming = new Person(); //Person ming是定义Person类型的变量ming,而new Person()是创建Person实例。
-
访问实例变量可以用 变量.字段
ming.name = "Xiao Ming"; // 对字段name赋值
-
多个
instance
拥有class
定义的字段,且各自都有一份独立的数据,互不干扰
方法 method
-
外部代码不能直接修改
private
字段,但是外部代码可以调用public方法来间接修改private
字段。 -
一个类通过定义方法,就可以给外部代码暴露一些操作的接口,同时,内部自己保证逻辑一致性。
-
调用方法的语法是实例变量.方法名(参数)
public class Main { public static void main(String[] args) { Person ming = new Person(); ming.setName("Xiao Ming"); // 调用方法设置name } } class Person { private String name; public void setName(String name) { this.name = name; } }
-
定义方法:
修饰符 方法返回类型 方法名(方法参数列表) {//可无参数 若干方法语句; return 方法返回值;//void不用返回 }
-
this
-
在方法内部,可以使用一个隐含的变量
this
,它始终指向当前实例 -
如果没有命名冲突,可以省略
this
,如果有局部变量和字段重名,那么局部变量优先级更高,就必须加上this
class Person { private String name; public String getName() { return name; // 相当于this.name } }
class Person { private String name; public void setName(String name) { this.name = name; // 前面的this不可少,少了就变成局部变量name了 } }
-
-
可变参数
- 用 类型… 定义,可变参数相当于数组类型
class Group { private String[] names; public void setNames(String... names) { this.names = names; } } Group g = new Group(); g.setNames("Xiao Ming", "Xiao Hong"); // 传入2个String g.setNames("Xiao Ming"); // 传入1个String g.setNames(); // 传入0个String,接收到的实际值是一个空数组而不是null
- 也可以把可变参数改写为
String[]
类型,但是,调用方需要自己先**构造String[]
**并传入方法,比较麻烦
class Group { private String[] names; public void setNames(String[] names) { this.names = names; } } Group g = new Group(); g.setNames(new String[] {"Xiao Ming", "Xiao Hong", "Xiao Jun"}); // 传入1个String[]
Group g = new Group(); g.setNames(null);//传入为空不是空数组
-
参数的传递
- 用方把参数传递给实例方法时,调用时传递的值会按参数位置一一绑定。
- 基本类型参数的传递,是调用方值的复制。双方各自的后续修改,互不影响。
- 引用类型参数的传递,调用方的变量,和接收方的参数变量,指向的是同一个对象。双方任意一方对这个对象的修改,都会影响对方(因为指向同一个对象,该对象类似存的地址)
构造方法
-
构造方法能在创建
Person
实例的时候,一次性传入name
和age
,完成初始化: -
构造方法的名称就是类名。构造方法的参数没有限制,在方法内部,也可以编写任意语句。但是,和普通方法相比,构造方法没有返回值(也没有
void
),调用构造方法,必须用new
操作符。public class Main { public static void main(String[] args) { Person p = new Person("Xiao Ming", 15); } } class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } }
-
任何
class
都有构造方法,如果一个类没有定义构造方法,编译器会自动生成一个默认构造方法,它没有参数,也没有执行语句,引用类型的字段默认是null
,数值类型的字段用默认值,int
类型默认值是0
,布尔类型默认值是false
,也可以对字段直接进行初始化:class Person { private String name; // 默认初始化为null public Person() { } }//or class Person { private String name = "Unamed";//对字段直接初始化 }
-
如果既要能使用带参数的构造方法,又想保留不带参数的构造方法,那么只能把两个构造方法都定义出来
public class Main { public static void main(String[] args) { Person p1 = new Person("Xiao Ming", 15); // 既可以调用带参数的构造方法 Person p2 = new Person(); // 也可以调用无参数构造方法 } } class Person { private String name; private int age; public Person() { } public Person(String name, int age) { this.name = name; this.age = age; } }
-
Java创建对象实例的时候,先初始化字段,再执行构造方法的代码进行初始化
-
多构造方法
- 可以定义多个构造方法,在通过
new
操作符调用的时候,编译器通过构造方法的参数数量、位置和类型自动区分(与重载函数有点像)
- 可以定义多个构造方法,在通过
方法重载
- 方法名相同,但各自的参数不同,称为方法重载(
Overload
) - 方法重载的返回值类型通常都是相同的
继承
extends
-
继承可以让一个类获得了另一个类的所有功能,我们只需要为前者编写新增的功能。
-
Java使用**
extends
**关键字来实现继承:class Person { private String name; public void setAge(int age) {...} } class Student extends Person { // 不要重复name和age字段/方法, // 只需要定义新增score字段/方法: private int score; public int getScore() { … } }
-
子类自动获得了父类的所有字段,严禁定义与父类重名的字段
-
在OOP的术语中,我们把
Person
称为超类(super class),父类(parent class),基类(base class),把Student
称为子类(subclass),扩展类(extended class)
继承树
-
在Java中,没有明确写
extends
的类,编译器会自动加上extends Object
-
一个类有且仅有一个父类。只有
Object
特殊,它没有父类。 -
一个类可以有多个子类
-
父类与子类之之间构成继承树
protected
-
子类无法访问父类的
private
字段或者private
方法 -
我们可以把
private
改为protected
。此时就可以被子类访问ps(如果不确定是否需要
public
,就不声明为public
,即尽可能少地暴露对外的字段和方法) -
**
protected
关键字可以把字段和方法的访问权限控制在继承树内部
super 父类
-
子类引用父类的字段时,可以用 super.fieldName(=this.name)
-
任何
class
的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句super();
class Person { protected String name; public Person(String name) { this.name = name; } } class Student extends Person { protected int score; public Student(String name, int age, int score) { super(); // 自动调用父类的构造方法 this.score = score; } }//Person类并没有无参数的构造方法,因此,编译失败。
-
子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。
阻止继承 sealed
-
使用
sealed
修饰class,并通过permits
明确写出能够从该class继承的子类名称。public sealed class Shape permits Rect, Circle, Triangle { ... } //上述Shape类就是一个sealed类,它只允许指定的3个类继承它: Rect, Circle, Triangle
-
sealed类在Java 15中目前是预览状态,要启用它,必须使用参数 --enable-preview 和 --source 15
-
class没有
final
修饰符,那么任何类都可以从该class继承,有final修饰时,该类不可被继承
向上转型(子类->父类)
-
把一个子类类型安全地变为父类类型的赋值,被称为向上转型(upcasting)
Student s = new Student(); Person p = s; // upcasting, ok Object o1 = p; // upcasting, ok Object o2 = s; // upcasting, ok
继承树是
Student > Person > Object
,所以,可以把Student
类型转型为Person
,或者更高层次的Object
。
向下转型(父类->子类)
-
不能把父类变为子类,因为子类功能比父类多,多的功能无法凭空变出来。
Person p1 = new Student(); // upcasting, ok Person p2 = new Person(); //此时p1是student型,p2是person型 Student s1 = (Student) p1; // ok Student s2 = (Student) p2; // runtime error,person为父类,student为子类!
-
为了避免向下转型出错,Java提供了
instanceof
操作,实际上判断一个变量所指向的实例是否是指定类型,或者这个类型的子类。如果一个引用变量为null
,那么对任何instanceof
的判断都为falseObject obj = "hello"; if (obj instanceof String) { String s = (String) obj; System.out.println(s.toUpperCase()); }
-
Java 14开始,判断
instanceof
后,可以直接转型为指定变量,避免再次强制转型... if (obj instanceof String s) { // 可以直接使用变量s: System.out.println(s.toUpperCase()); }
区分继承和组合
-
is关系用继承,has关系用组合
class Book { protected String name; } class Student extends Book{};//不合理,student has book class Student extends person{ protected Book book; };//合理,student is person,Student可以持有一个Book实例
方法签名
- 方法声明的两个组件构成了方法签名 :方法的名称和参数类型
多态 Polymorphic
-
多态是指针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法,而非变量的声明类型
//例1: public class Main { public static void main(String[] args) { Person p = new Student(); p.run(); // 应该打印Person.run还是Student.run? } } class Person { public void run() { System.out.println("Person.run"); } } class Student extends Person { @Override public void run() { System.out.println("Student.run"); } } // 调用student的run()方法,打印student。run //例2: public void runTwice(Person p) { p.run(); p.run(); } //无法确定调用的是不是Person类定义的run()方法, //因为我们是无法知道传入的参数实际类型究竟是Person,还是Student,还是Person的其他子类
-
允许添加更多类型的子类实现功能扩展,却不需要修改基于父类的代码。
public class Main { public static void main(String[] args) { Income[] incomes = new Income[] { new Income(3000), new Salary(7500), }; System.out.println(totalTax(incomes)); } //计算工资 public static double totalTax(Income... incomes) { double total = 0; for (Income income: incomes) { total = total + income.getTax(); } return total; } } class Income{} class Salary extends Income{//salary的构造方法可以直接调用父类的构造方法,用super(实参) public double getTax() { ... return (income - 5000) * 0.2; } } //totalTax()方法只需要和Income打交道,它完全不需要知道Salary存在,就可以正确计算出总的税。如果我们要新增一种稿费收入,只需要从Income派生,然后正确覆写getTax()方法就可以。把新的类型传入totalTax(),不需要修改任何代码。
覆写Object方法
- Object定义了几个重要的方法:(有点库函数那意思,但不可以像库函数那样直接调用)
- toString():把instance输出为String;
- equals():判断两个instance是否逻辑相等;
- hashCode():计算一个instance的哈希值。
覆写与重载 override与overload
-
方法签名不同,就是Overload(方法重载),是一个新方法
-
方法签名相同,并且返回值也相同,就是
Override
(覆写) -
方法名相同,方法参数相同,但方法返回值不同,也是不同的方法
-
父类中定义的方法名称是其他程序认可的,但是子类使用时发现,原有方法不能满足现在的操作要求,但要保留原方法名称,因此出现了覆写
-
加上
@Override
可以让编译器帮助检查是否进行了正确的覆写public person{} public class Student extends Person { @Override //检查 public void run(String s) {} }
调用super
- 在子类的覆写方法中,用super来调用父类的被覆写的方法‘
- super(实参)
final 不允许覆写,继承,修改
-
用final修饰的方法不能被覆写
public final String hello() {}
-
用final修饰的类不能被继承
final class Person {}
-
用final修饰的字段在初始化后不能被修改
public final String name = "Unamed";//对final字段重新赋值会报错
-
在构造方法中初始化final字段更为常用,因为可以保证实例一旦创建,其final字段就不可修改
-
用final修饰局部变量可以阻止被重新赋值:
抽象类
-
如果父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它,那么,可以把父类的方法声明为抽象方法:
(abstract)class Person { public abstract void run(); }
-
Person
类无法被实例化。编译器会告诉我们,无法编译Person
类,因为它包含抽象方法,必须把Person
类本身也声明为abstract
,才能正确编译它Person p = new Person(); // 抽象类无法实例化,编译错误
-
抽象类可以强迫子类实现其定义的抽象方法,否则编译会报错。因此,抽象方法实际上相当于定义了**“规范”**,即子类必须覆写父类的抽象方法
面向抽象编程
-
尽量引用高层类型,避免引用实际子类型的方式,称之为面向抽象编程。
-
我们对抽象类进行方法调用,并不关心父类变量的具体子类型
Person s = new Student(); Person t = new Teacher(); // 不关心Person变量的具体子类型,person的run方法为空 s.run();** t.run();
接口 interface
-
如果一个抽象类没有字段,所有方法全部都是抽象方法,就可以把该抽象类改写为接口:
interface
-
接口不能有字段,因为接口定义的所有方法默认都是
public abstract
的 -
当一个具体的
class
去实现一个interface
时,需要使用implements
关键字interface Person { void run(); } class Student implements Person { private String name; public Student(String name) { this.name = name; } @Override public void run() { System.out.println(this.name + " run"); } }
-
一个类只能继承自另一个类,不能从多个类继承。但是,一个类可以实现多个
interface
class Student implements Person, Hello { // 实现了两个interface }
-
Java的接口特指
interface
的定义,表示一个接口类型和一组方法签名,而编程接口泛指接口规范,如方法签名,数据格式,网络协议等[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-69aaF7OT-1653919522744)(C:\Users\Jelovese\Desktop\Java学习笔记\屏幕截图 2021-10-08 195541.png)]
default方法
public interface A {
public default void a(){
System.out.println("这是A");
}
}
- 不是必须重写父类所有方法的,分为以下两种情况: 父类方法为抽象方法时,子类必须重写(实现)所有父类的抽象方法; 父类方法为普通方法时,子类可以重写父类方法,也可以不重写
- 实现类可以不必覆写
default
方法。default
方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是default
方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法 - 如果一个类同时实现接口A和B,接口A和B中有相同的default方法,这时,该类必须重写接口中的default方法
接口继承
-
interface继承自interface使用extends,相当于扩展了接口的方法。
interface Hello {...} interface Person extends Hello {...}
继承关系 ???
- 公共逻辑适合放在
abstract class
中,具体逻辑放到各个子类,而接口层次代表抽象程度 - 在使用的时候,实例化的对象永远只能是某个具体的子类,但总是通过接口去引用它,因为接口比抽象类更抽象:
静态
静态字段
-
在一个
class
中定义的字段,我们称之为实例字段。实例字段的特点是,每个实例都有独立的字段,各个实例的同名字段互不影响。 -
实例字段在每个实例中都有自己的一个独立“空间”,但是静态字段只有一个共享“空间”,所有实例都会共享该字段,共享一个位置
class Person { public static int number; }
-
无论修改哪个实例的静态字段,所有实例的静态字段都被修改了,原因是静态字段并不属于实例
-
不推荐用**
实例变量.静态字段
** 去访问静态字段,因为实例对象并没有静态字段。在代码中,实例对象能访问静态字段只是因为编译器可以根据实例类型自动转换为类名.静态字段(推荐使用)
来访问静态对象
静态方法
-
调用实例方法必须通过一个实例变量,而调用静态方法则通过类名就可以调用。静态方法类似其它编程语言的函数
public static void main(String[] args) { Person.setNumber(99);//调用静态方法 } class Person { public static int number; public static void setNumber(int value) { number = value; } }
-
静态方法不属于实例,因此,静态方法内部,无法访问
this
变量,也无法访问实例字段,它只能访问静态字段。 -
通过实例变量也可以调用静态方法,但是和访问静态字段原理相同
-
Java程序的入口
main()
也是静态方法
接口的静态字段
-
interface
是一个纯抽象类,所以它不能定义实例字段 -
interface
的静态字段必须为public static final
类型;编译器会自动加上public static final,所以我们只需要写数据类型就可以
包 package
-
使用
package
来解决名字冲突 -
包是一种名字空间,一个类总是属于某个包,类名真正的完整类名是 包名.类名
-
定义
class
的时候,我们需要在第一行声明这个class
属于哪个包package ming; // 申明包名ming public class Person {}
-
包可以是多层结构,但没有父子关系,没有继承关系
-
没有定义包名的
class
,它使用的是默认包(不推荐) -
编译后的
.class
文件也需要按照包结构存放,使用ide把编译后的.class
文件放到bin
目录下,那么,编译的文件结构就是[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RYymQizd-1653919522745)(C:\Users\Jelovese\Desktop\Java学习笔记\屏幕截图 2021-10-10 203740.png)]
-
所有Java文件对应的目录层次要和包的层次一致,以
package_sample
作为根目录,src
作为源码目录,那么所有文件结构就是:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-djRnf2pq-1653919522745)(C:\Users\Jelovese\Desktop\Java学习笔记\屏幕截图 2021-10-10 203819.png)]
包作用域
- 位于同一个包的类,可以访问包作用域的字段和方法。不用
public
、protected
、private
修饰的字段和方法就是包作用域。
import
-
在一个类中引用其他包的类的三种方法
-
直接写出完整类名, 包名.类名
-
用
import
语句,导入想要引用的类,然后写简单类名在写
import
的时候,可以使用*
,表示把这个包下面的所有class
都导入进来**(但不包括子包的class
,因为包没有继承关系)**package ming; // 导入完整类名: mr.jun.Arrays arrays = new mr.jun.Arrays(); //第二种方法 import mr.jun.Arrays; // import mr.jun*;表示把包mr.jun下面的所以class都导入进来(不推荐) public class Person { public void run() { Arrays arrays = new Arrays(); } }
-
import static
可以导入一个类的静态字段和静态方法(很少使用)// 导入java.lang包的System类的所有静态字段和静态方法: import static java.lang.System.*;
-
-
编译器默认自动
import
当前package
的其他class
; -
编译器默认自动
import java.lang.*
-
如果有两个
class
名称相同,那么只能import
其中一个,另一个必须写完整类名。
最佳实践
- 为了避免名字冲突,我们需要确定唯一的包名。推荐的做法是使用倒置的域名来确保唯一性(ex:com.liaoxuefeng.sample)
- 要注意不要和
java.lang
包的类重名,也不要和JDK常用类重名
作用域
-
定义为
public
的class
、interface
可以被其他任何类访问,无须导入定义为
public
的field
、method
可以被其他类访问,前提是首先有访问class
的权限package abc; public class Hello { public void hi() {} } package xyz; class Main { void foo() { Hello h = new Hello();//访问public class h.hi();//访问 public field } }
-
定义为
private
的field
、method
,访问权限被限定在class
的内部,而且与方法声明顺序无关 -
一个类内部还定义了另外一个类(称之为嵌套类或者内部类
nested class
),那么嵌套类拥有访问private
的权限 -
使用局部变量时,应该尽可能把局部变量的作用域缩小,尽可能延后声明局部变量
内部类的分类简单介绍
Inner class
-
一个类定义在另一个类的内部,这个类就是Inner Class:
-
实例不能单独存在,必须依附于上一个 Class的实例
Outer.Inner inner = outer.new Inner();//Inner是Outer的内部类