面向对象

对于类和对象的理解有了一定程度的提升

好好总结一下经验,毕竟,谁不得和几个对象打交道呢

面向对象基础

1. 面向对象思想

面向过程关注的执行过程,面向对象关注的是具备功能的对象

面向过程到面向对象,是程序员思想上从执行者到指挥者的转变

  • 三大思想:
    • OOA:面向对象分析(Object Oriented Analysis)
    • OOD:面向对象设计(Object Oriented Design)
    • OOP:面向对象程序(Object Oriented Programming)
  • 三大特征:
    • 封装性:所有的内容对外部不可见
    • 继承性:将其他的功能继承下来继续发展
    • 多态性:方法的重载本身就是一个多态性的体现

2. 类和对象的创建

  • 类和对象的概念:类表示一个共性的产物,是对一类事物抽象出的综合特征,而对象,是一个个性的产物,是一个个体的特征。

2.1 类的创建

  • 注意点:

    • 类必须编写在.java文件中
    • 一个.java文件可以包含N个类,但是只能有一个public声明的类
    • .java文件的文件名必须和public声明的类的类名保持一致
  • 定义格式

    1
    2
    3
    4
    class 类名称{
    成员属性
    成员方法
    }
  • 属性和方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    1.属性定义格式:
    数据类型 属性名;
    2.属性定义并赋值的格式:
    数据类型 属性名 = 初始化值;
    3.方法定义格式:
    权限修饰符 返回值类型 方法名(形式参数列表){
    //方法体
    return 返回值;
    }

2.2 对象的创建与使用

1
2
3
4
5
一个类要想真正的进行操作,则必须依靠对象,对象的定义格式如下:
类名称 对象名称 = new 类名称() ;
如果要想访问类中的属性或方法(方法的定义),则可以依靠以下的语法形式:
访问类中的属性: 对象.属性 ;
调用类中的方法: 对象.方法(实际参数列表) ;

2.3 创建对象内存分析

image-20210223213536082

先将类加载到方法区,每次创建新对象时将对象信息存放到堆中,声明引用类型变量并赋值的过程,其实就是将对象在堆内存中的首地址存放在栈中。

对象的赋值操作只是将地址复制(浅拷贝),虽然变量名不同,但内容(地址)相同,指向同一块堆内存空间

2.4 构造方法

  • 作用:对象初始化
  • 特点:
    • 每个java类都至少有一个构造方法
    • 未明确编写构造方法,编译器会自动生成一个无参的构造方法
    • 只要编写构造方法,就不会再自动生成无参构造器
  • 注意:不要依赖自动生成无参构造器的方法,无参构造器也要手动编写
  • 定义的格式:与普通方法基本相同, 区别在于: 方法名称必须与类名相同, 没有返回值类型的声明 !

3. 方法的重写与重载

3.1 重载

在leetcode中可以经常看到构造器的重载

  • 重载的格式:

    方法名称相同, 参数类型或参数长度不同,或者参数类型的顺序不同, 可以完成方法的重载 ! 方法的重载与返回值无关!

  • 重载的目的:

    方法的重载 ,可以让我们在不同的需求下, 通过传递不同的参数调用方法来完成具体的功能。

    1
    2
    3
    4
    5
    6
    7
    8
    class Math{
    int sum(int x, int y){
    return x+y;
    }
    double sum(double x, double y){
    return x+y;
    }
    }

    如上,两种方法实现的功能其实都是求和,方法重载使得整形和双精度浮点型数据的求和都可以通过调用sum()方法实现

3.2 构造方法的重载

构造方法的重载 ,可以让我们在不同的创建对象的需求下, 调用不同的方法来完成对象的初始化!

1
2
3
4
5
6
7
8
9
10
11
class Person{
person(String name1, int age1){
name = name1;
age = age1;
}
person(String name2){
name = name2;
}
String name;
String age;
}

4. 匿名对象

不给对象起名,也就是不再用引用类型变量接收新建对象,此时对象只在堆内存中被创建并被一次性使用!

1
2
3
4
5
6
7
8
9
class Math{
int sum(int x, int y){
return x+y;
}
}
Math math1 = new Math();
int ans = math1.sum(1,2);
//假如只调用一次sum方法,就可以使用匿名对象
int ans = new Math().sum();

new Math()就是我们创建的对象,创建完成后直接调用方法。

面向对象进阶

1. 三大特性之-封装

1.1 封装的三大问题

  • 封装的原则:

    隐藏对象的属性和实现细节,仅对外公开访问方法,并且控制访问级别

  • 封装的意义:

    保护或防止代码(数据)被无意间破坏;保护成员属性,不让类以外的程序直接访问或修改

  • 封装的实现:

    用访问权限修饰符private对属性进行修饰

注意:开发中为了避免出现逻辑错误, 我们建议对所有属性进行封装,并为其提供setter及getter方法进行设置和取得操作。

2. this

在Java基础中,this关键字是一个最重要的概念。使用this关键字可以完成以下的操作:

  • 调用类中的属性:this.属性名
  • 调用类中的方法或构造方法:
    • 方法:this.方法名()
    • 构造方法:this();
    • 注意:在构造方法中,调用其他构造方法时,调用语句必须编写在当前构造方法的第一行(如下例)
  • 表示当前对象:之前的代码编写中没用this,但是本质是一样的,默认是加了this,也即类内调用属性和方法时可以省略this
1
2
3
4
5
6
7
8
9
10
11
class Person{
private String name;
private int age;
person(){
this("默认姓名"1);
}
person(String name, int age){
this.name = name;
this.age = age;
}
}

3. static*

image-20210224172220212

  • static的概念:

    static表示“静态”的意思,可以用来修饰成员变量和成员方法(后续还会学习 静态代码块 和 静态内部类)。

    如上图所示,实例变量在对象创建时被创建,在对象回收时被销毁,也就是跟着对象动态的创建和丢弃,和一上来就在方法区中开辟内存的静态变量相对比。每个创建的对象存有该静态变量的地址。

  • static的作用:

    static的主要作用在于创建独立于具体对象的域变量或者方法

  • 如何理解独立于对象?(见上图)

    被static关键字修饰的方法或者变量不需要依赖于对象来进行访问,只要类被加载了,就可以通过类名去进行访问。
    并且不会因为对象的多次创建而在内存中建立多份数据

  • 私有静态变量:private static

    只能在本类使用且不被实例对象调用

    共有静态变量:public static

    该类的所有实例化对象共享该类变量,实例化时不生成副本

1
2
3
1. 静态成员 在类加载时加载并初始化。
2. 无论一个类存在多少个对象, 静态的属性, 永远在内存中只有一份(可以理解为所有对象公用 )
3. 在访问时: 静态不能访问非静态 , 非静态可以访问静态 !

使用方法例1:使用静态变量进行对象创建的计数

1
2
3
4
5
6
7
8
9
10
11
class Clothes {
static int count;
private int num;
Clothes(){
++count;
num = count;
}
public int getNum(){
return num;
}
}

如何理解:静态不能访问非静态 , 非静态可以访问静态 ??

1
2
3
4
5
6
7
8
9
10
class Person{
person(){}
void say(){
say1();
System.out.println("玛卡巴卡");
}
static void say1(){
System.out.println("阿巴阿巴");
}
}

很容易理解:静态方法say1()在类加载时,也就是对象创建前就被加载到内存中,因此成员方法say()可以实现调用,反过来在对象还没创建呢,对象方法自然不存在,谈何被加载到内存呢!

4. 权限修饰符

影响范围:√:能看到;×:看不到

访问控制修饰符 本类 同一个包的类 继承类 其他包
private × × ×
default(friendly) x ×
protected ×
public

5. 代码块

代码块笼统的理解:在两个匹配的大括号之间的代码都可以理解为是一个代码块

  • 普通代码块

    在执行的流程中出现的 代码块, 我们称其为普通代码块。

  • 构造代码块

    在类中的成员代码块,我们称其为构造代码块,在每次对象创建时执行一次,执行在构造方法之前

    • 意义:

      因为构造方法的重载,有些构造方法可能不被执行,当有创建对象时必须要执行的操作,可以用其编写在构造代码块中。

  • 静态代码块:

    在对象未创建时就可以执行,因为类只加载一次,静态代码块也就只执行一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person{
private String name;
private int age;
//构造代码块
{
System.out.println("创建时执行1");
}
//静态代码块
static {
System.out.println("直接执行");
}
person(){
System.out.println("创建时执行2");
}
person(String name, int age){
this.name = name;
this.age = age;
}
}

问:构造方法 与 构造代码块 以及 静态代码块的执行顺序?

6. main方法详解

  • main:系统规定好的方法名称。如果main写错了或没有,会报错:NoSuchMethodError: main

  • String[] args:字符串数组,接收参数的

    1
    2
    3
    4
    5
    6
    7
    public class StaticDemo10{
    public static void main(String args[]){
    for(int i=0;i<args.length;i++){
    System.out.println(args[i]) ;
    }
    }
    }

    所有的参数在执行类的时候以空格进行分割。
    java StaticDemo10 1 2 3 4 5 6 7
    但是,如果现在我要输入的是以下几种参数“hello world”、“hello vince”、“hello mjw”。

image-20210224210912945

面向对象高级

1. 三大特性之-继承

  • 继承的概念:

    java面向对象技术的一块基石,它允许创建分等级层次的类;

  • 继承的作用:

    子类继承父类的特征和行为,使得子类对象具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为

1.1 内存关系

image-20210225185339939

1
2
3
4
5
//Student类继承自Person类
Student s = new Student(); //首先在堆内存中自动创建一个Person对象,然后才创建Studnet对象,通过super变量保存Person首地址
//即可以将Person看做Student的一部分
s.setName("张三"); //先找Student类中有没有setName方法(方法的重写),没有就调用super的setName()方法,改变的其实是 //Person对象中的变量
s.say();

可以看出,继承即是子类拥有了父类对象的一个地址而已,或者说super就是父类对象名

注意:权限修饰符在继承中的作用,父类中只有public和protected修饰的属性,才能被子类直接调用,否则只能通过set和get

1.1.1 子类和父类加载关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Parent
{
public static int parent;
static
{
System.out.println("This is Parent");
}
}
public class Child extends Parent
{
public static int child;
static
{
System.out.println("This is Child");
}
}
  • 通过子类访问子类静态变量child:Child.child;

    1
    2
    3
    This is Parent  //先调用父类的静态构造代码块
    This is Child //再加载子类
    0
  • 通过父类访问父类的静态变量parent:

    1
    2
    This is Parent  //只加载父类
    0
  • 通过子类访问父类的静态变量parent:Child.parent;

    1
    2
    This is parent  //同样是只加载父类
    0

    可见:通过子类访问父类的静态变量/方法,只需要将父类加载到方法区,并不会加载子类

1.2 super

  • super的作用:

    通过super,可以访问父类的构造方法,属性和方法

    问题:访问的格式是怎样的,可以省略super,直接调用吗?

  • 调用父类的构造方法:

    子类对象创建时需要先自动创建一个父类对象,前提是需要父类具有一个无参构造方法,子类其实会默认在每个构造方法中调用父类的构造方法,如下所示:

    注意:super调用父类构造方法必须写在子类构造方法的第一行(和this()调用狗仔不能同时存在,因为this()也会调用父类的构造方法,所以并不存在冲突)

    1
    2
    3
    4
    5
    class Student{
    public Studen(){
    super();
    }
    }

    问题:如何调用父类的有参构造方法,完成父类对象的构造呢

    很简单,通过super()传参就可以调用父类的有参方法

    1
    2
    3
    4
    5
    class Student{
    public Studen(){
    super("无姓名"1);
    }
    }

1.3 重写

  • 重写的规则:
    • 参数列表和返回类型必须相同
    • 访问权限不能比父类方法更低(例如父类权限为public,子类的重写方法就不能为protected)
    • 父类的成员方法只能被它的子类重写
    • 声明为static的private的方法不能被重写,但能够被再次声明

重写和重载的区别?

  • 发生位置:子父类中; 同一个类中
  • 参数列表:完全相同;类型,长度,或者类型顺序不同
  • 返回值类型:完全相同; 无关
  • 访问权限: 子类不能比父类低; 无关
  • 异常处理: 子类的异常范围可以更小,但不能抛出新的异常; 和异常无关

2. final

  • 可修饰的对象:

    属性,变量,方法,类

  • final修饰属性和变量

    变量会变成常量,无法再进行赋值;

    final修饰的局部变量,只能赋值一次(可以先声明后赋值)

    final修饰的是成员属性,必须在声明时赋值(成员属性的默认值为0)

    1
    2
    final int a = 1;
    a++; // 报错
  • final修饰的类:不能被继承

  • final修饰的方法:不能被重写

3. 抽象类

  • 概念(基于抽象方法):

    抽象类必须使用abstract class声明,一个抽象类中可以没有抽象方法。但抽象方法必须写在抽象类或者接口中。

    抽象类不能被实例化

  • 抽象方法的概念:

    只声明而未实现的方法称为抽象方法(未实现指的是:没有“{}”方法体),抽象方法必须使用abstract关键字声明

  • 抽象类的实现(不是实例化):

    抽象类本身是不能直接进行实例化操作的,即:不能直接使用关键字new完成。

    一个抽象类必须被子类所继承,被继承的子类(如果不是抽象类)则必须覆写(重写)抽象类中的全部抽象方法。

3.1 常见问题

  • 抽象类能否被final声明?

    不能,final修饰的类不能有子类

  • 抽象类能否有构造方法?

    能有构造方法,而且子类对象实例化的时候的流程与普通类的继承是一样的,都是要先调用父类中的构造方法(默
    认是无参的),之后再调用子类自己的构造方法。

  • 抽象类是不是必须要求抽象方法

    答:不是!

3.2 抽象类和普通类的区别

  • 抽象类必须用public或protected修饰,默认缺省为public
  • 抽象类不可以使用new关键字创建对象, 但是在子类创建对象时, 抽象父类也会被JVM实例化。
  • 如果一个子类继承抽象类,那么必须实现其所有的抽象方法。如果有未实现的抽象方法,那么子类也必须定义为
    abstract类

4. 接口

概念:

如果一个类中的全部方法都是抽象方法,全部属性都是全局常量,那么此时就可以将这个类定义成一个接口。

1
2
3
4
interface 接口名称{
全局常量 ;
抽象方法 ;
}
  • 全局常量和抽象方法的简写

    因为接口本身都是由全局常量和抽象方法组成,所以接口中的成员定义可以简写:

    • 全局常量编写时, 可以省略public static final 关键字
    • 抽象方法编写时, 可以省略 public abstract 关键字
  • 接口实现和继承

    • 实现:支持多实现

      1
      2
      3
      class 子类 implements 父接口1,父接口2...{
      //如果不能完全实现,同样可以声明抽象类来继承
      }
    • 继承:允许多继承

      1
      2
      3
      interface C extends A, B, C, D{
      //均为抽象方法或全局常量
      }

5. 三大特性之-多态

概念:多态就是对象的多种表现形式(多种体现形态)

5.1多态的体现

  • 对象的多态性,从概念上非常好理解,在类中有子类和父类之分,子类就是父类的一种形态 ,对象多态性就从此而来。
  • 方法的重写是多态的基础。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person{
public void say(){
System.out.println("吃了没");
}
}
class Student extends Person{
public void say(){
System.out.println("好好学习,天天向上");
}
}
class Test{
public static void main(Strign args[]){
Person p = null;
Studetn s = new Student();
p = s;
p.say();
}
}

输出结果:好好学习,天天向上

理解: 学生对象s就可以看作是人类对象p的一种形态

5.2 多态中对象的类型转换

类似于基本数据类型的转换:

  • 向上转型:将子类实例变为父类实例
    格式:父类 父类对象 = 子类实例

    注意:在这种情况下生成的实例只能调用父类方法或子类重写的父类方法,不能访问子类独有的方法

  • 向下转型:将父类实例变为子类实例(可先通过instanceOf判断原生的类或接口)

    格式:子类 子类对象 = (子类)父类实例

    注意:只有通过这种强转的方式,才能使实例对象访问子类独有方法

1
2
3
4
5
//Student,Nurse均是继承自Person的子类
Person p1 = new Student();
Person p2 = new Nurse();
Student s1 = (Student)p1; //ok,将子类转型为父类
Student s2 = (Studetn)p2; //wrong,p1的本质还是学生对象,p2的本质还是护士对象,所以p1可以转,但p2不能转

5.3 多态的应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Person{
void say();
}
class Student implements Person{
public void say(){
System.out.println("好好学习");
}
}
class Test{
public static void main(String[] args){
Student s = new Student();
say(s);
}
public void say(Person p){
p.say();
}
}

Test类中的say()方法,所有实现Person接口的类的实例都可以作为其参数,避免了逻辑代码的变更。

5.4 instanceof

5.2中,Nurse对象转换的父类对象,再转换为Student类型时报错。所以,对于一个未知的Person p,我如何知道这个p到底是哪种形态,或者说是哪种子类的实例

答:instanceof关键字

格式:
实例化对象 instanceof 类 //此操作返回boolean类型的数据

1
2
3
Person p1 = new Student();
Person p2 = new Nurse();
p2 instanceof Student //返回值为false

6. Object类

Object类是所有类的父类,如果没有继承某个具体的类,则一定默认继承了Object类

  • Object类的多态

    使用Object可以接收任意的引用数据类型(所有的类追根溯源都能找到Object,根据多态)

6.1 Object类的常用方法

  • toString:建议将所有类的toString方法进行重写
  • equals:equals和==的区别
    • ==可以比较基本数据类型
    • 当对比引用数据类型时,equals本身和==相同,要求引用类型的数据相同,即两对象的地址相同,一般对equals进行重写
  • equals重写的特性:
    • 自反性
    • 对称性
    • 传递性
    • 一致性
    • 非空性

7. 内部类*

  • 概念:在Java中,可以将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类。
    广泛意义上的内部类一般来说包括这四种:
    1、成员内部类
    2、局部内部类
    3、匿名内部类
    4、静态内部类

7.1 成员内部类

image-20210226165137063

  • 在内部类中使用与外部类同名的属性、方法时,默认情况下访问的是成员内部类的成员。

    如果要访问外部类的同名成员,需要以下面的形式进行访问:外部类.this.成员变量/方法

  • 在外部使用成员内部类:

    1
    2
    Outter outter = new Outter();
    Outter.Inner inner = outter.new Inner();

7.2 局部内部类

局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内

注意:局部内部类就像是方法里面的一个局部变量一样,是不能有public、protected、private以及static修饰符的。

7.3 匿名内部类

匿名内部类由于没有名字,所以它的创建方式有点儿奇怪。创建格式如下:

1
2
3
4
new 父类构造器(参数列表)|实现接口()
{
//匿名内部类的类体部分
}
  • 必须且仅能继承一个父类或接口
  • 没有class关键字,只能一次性使用,生成一个实例对象的引用

使用匿名内部类的注意事项:

  • 使用匿名内部类时,只能继承一个类或者实现一个接口。
  • 匿名内部类中是不能定义构造函数的。
  • 匿名内部类中不能存在任何的静态成员变量和静态方法。
  • <font color = “blue”>匿名内部类为局部内部类</font>,所以局部内部类的所有限制同样对匿名内部类生效。
  • 匿名内部类不能是抽象的,它必须要实现继承的类或者接口的所有抽象方法
  • 只能访问final型的局部变量(这一条件对所有局部内部类均有作用)

为什么只能访问final型的局部变量?

因为局部内部类会被单独的编译为一个字节码文件,为了保证这个单独文件中备份的变量和外部的变量能够一直保持一致,就要求这个变量本身就是常量。

7.4 静态内部类

在成员内部类的基础上加一个static修饰,类似于静态构造代码块

规则,静态内部类不能调用外部的实例变量或方法

8. 包装类

8.1 8种包装类

image-20210226210511693

  • 分为两个类型:

    • Number:Integer、Short、Long、Double、Float、Byte都是Number的子类表示是一个数字。
    • Object:Character、Boolean都是Object的直接子类。
  • JDK1.5之后可以实现自动拆箱和自动装箱(可以进行四则运算和自增自减操作)

    1
    2
    3
    4
    Float f = 10.3f ; // 自动装箱
    float x = f ; // 自动拆箱
    System.out.println(f * f) ; // 直接利用包装类完成
    System.out.println(x * x) ; // 直接利用包装类完成

9. 可变参数

一个方法中定义完了参数,则在调用的时候必须传入与其一一对应的参数,但是在JDK 1.5之后提供了新的功能,可以根据需要自动传入任意个数的参数。
语法:

1
2
3
4
5
6
7
8
9
10
返回值类型 方法名称(数据类型…参数名称){
//参数在方法内部 , 以数组的形式来接收
}
int sum(int... nums){
int ans = 0;
for(int i = 0; i<nums.length; ++i){
ans += nums[i];
}
return ans;
}

注意:可变参数只能出现在参数列表的最后。