Loading

Java学习笔记02-面向对象

1. 面向对象(初级)

1.1 类与对象

类是抽象的,概念的,代表一类事物,是数据类型;
对象是具体的,实际的,代表一个具体事物,是实例;
类是对象的模板,对象是类的一个个体,对应一个实例。

public class Object01 {
	public static void main(String[] args) {
		Cat cat1 = new Cat(); //创建对象
		cat1.name = "the white";
		cat1.age = 3;
		cat1.color = "white";

		Cat cat2 = new Cat();
		cat2.name = "the flower";
		cat2.age = 100;
		cat2.color = "flower";

		System.out.println("The first:"+cat1.name+" "+cat1.age+" "+cat1.color);
		System.out.println("The first:"+cat2.name+" "+cat2.age+" "+cat2.color);

	}
}

class Cat {
	//属性
	String name;
	int age;
	String color;
}

创建对象流程:

  1. 加载类信息(属性和方法,只加载一次);
  2. 在堆中分配空间,进行默认初始化,把地址赋给对象名;
  3. 进行指定初始化。

对象属性

概念上,属性 = 成员变量 = 字段。
属性是类的一个组成部分,可以是基本数据类型,也可以是引用类型。
属性定义:访问修饰符 属性类型 属性名;
属性如果不赋值,会有默认值。

对象方法

public class Object01 {
	public static void main(String[] args) {
		Person p1 = new Person();
		int calResult = cal01(1000);
	}
}

class Person {
	String name;
	int age;
	//方法
	public int cal01(int n) {
		int res = 0;
		for (int i=1; i<=n; i++) {
			res += 1;
		}
		return res;
	}
}

访问修饰符控制方法的使用范围;
返回值
一个方法最多有一个返回值,返回值类型可以任意;
如果方法有返回值,则最后必须有return语句,返回值类型必须统一(可存在自动类型转换);
void方法可以不写return语句或者只写return;
参数
一个方法可以有0个形参,也可有多个形参,中间用逗号隔开;
参数类型可以任意,传参时必须对应参数列表相同或兼容类型的参数;
形参实参对应顺序、个数必须一致;
方法调用
同一类中的方法可以直接调用;
跨类的方法需要通过对象名调用:A类调用B类方法,先在A类中创建B类对象再调用;
跨类的方法调用受到访问修饰符限制。

//克隆对象
public class Object01 {
	public static void main(String[] args) {
		Person p = new Person();
		p.name = "Bob";
		p.age = 19;

		MyTools tool = new MyTools();
		Person p2 = tool.copyPerson(p);

		System.out.println(p);
		System.out.println(p2);
	}
}

class Person {
	String name;
	int age;
}

class MyTools {
	public Person copyPerson(Person p) {
		Person p2 = new Person();
		p2.name = p.name;
		p2.age = p.age;

		return p2;
	}
}

1.2 方法重载(Overload)

Java中允许同一个类中,多个同名方法的存在,但要求形参列表不一致。
比如System.out.println()可以打印各种类型。

public class OverLoad01 {
	public static void main(String[] args) {
		MyCalculator mc = new MyCalculator();
		System.out.println(mc.calculate(1, 1.1));
		System.out.println(mc.calculate(1, 1, 1));
	}
}

class MyCalculator {
	public int calculate(int n1, int n2) {
		return n1 + n2;
	}

	public double calculate(int n1, double n2) {
		return n1 + n2;
	}

	public double calculate(double n1, int n2) {
		return n1 + n2;
	}

	public int calculate(int n1, int n2, int n3) {
		return n1 + n2 + n3;
	}
}

注意
重载的方法名必须相同;形参列表必须不同(类型或个数或顺序至少一样不同,参数名无要求);返回类型无要求。

1.3 可变参数

Java允许将同一个类中多个同名同功能但参数个数不同的方法封装成一个方法。
访问修饰符 返回类型 方法名(数据类型... 形参名){ }

public int sum(int... nums) {
    int res = 0;
    for (int i = 0; i < nums.length; i++) {
        res += nums[i];
    }
    return res;
}

注意

  1. 可变参数的实参可以0个或任意多个;
  2. 可变参数的实参可以是数组;
  3. 可变参数的本质就是数组;
  4. 可变参数可以和普通类型的参数一起放在形参列表,但必须保证可变参数在最后;
  5. 一个形参列表只能出现一个可变参数。

1.4 作用域

  1. Java中主要的变量就是属性(成员变量)和局部变量;
  2. 局部变量指在成员方法或代码块中定义的变量;
  3. 作用域的分类:全局变量(属性)作用域为整个类,局部变量作用域为其所在的代码块;
  4. 全局变量不赋值时有默认值;局部变量必须赋值后才能使用,无默认值。

注意

  1. 属性和局部变量可以重名,访问时遵循就近原则;
  2. 属性的生命周期较长,局部变量生命周期较短;
  3. 作用域范围:全局变量可以被本类或其他类使用(通过对象调用),局部变量只能在本类中对应的方法中使用;
  4. 修饰符:全局变量可以加修饰符,局部变量不可以加修饰符。

1.5 构造器

构造器constructor,也叫构造方法,是类的一种特殊方法,用于完成对新对象的初始化。
构造器修饰符可以默认也可以其他,构造器无返回值,构造器方法名和类名一致。
创建对象时,系统自动调用构造器完成对象初始化。

public class Constructor01 {
	public static void main(String[] args) {
		Person p1 = new Person("Smith", 30);
		System.out.println("p1 name:" + p1.name +" p1 age:" + p1.age);
	}
}
class Person {
	String name;
	int age;

	public Person(String pName, int pAge) {
		name = pName;
		age = pAge;
	}
}

注意
一个类可以定义多个构造器,即构造器重载;
如果没有手动定义构造器,系统自动给类生成一个默认无参构造器(使用javap **.class反编译查看);
一旦定义自己的构造器,默认构造器就被覆盖并无法使用,除非显式定义默认构造器才能使用,即Person(){}

1.6 this

JVM会给每个对象分配this,代表当前对象。
this指向的地址和其对象实例的地址相同。

public class This01 {
    public static void main(String[] args) {
        Person p1 = new Person("Tom", 29);
        Person p2 = new Person("Bob", 32);
    }
}

class Person {
	String name;
	int age;
    public Person() {
        this("jack", 10);
        System.out.println("T() constructor");
    }

	public Person(String name, int age) {
		this.name = name; //this.name指当前对象属性name
		this.age = age; //this.age指当前对象属性age
	}
}

this(参数列表)只能在构造器中调用,且必须放在第一条语句。
this不能在类定义的外部使用,只能在类定义的方法中使用。

2. 面向对象(中级)

2.1 包

包的作用:区分相同名字的类;当类很多时方便管理类;控制访问范围。
语法:package com.pcg1;
包的本质就是创建不同的文件夹保存类文件。

/* src
    |- com
        |- xiaoqiang
            |- Dog
        |- xiaoming
            |- Dog
        |- use
            |-Test
*/
package com.use;
import com.xiaoqiang.Dog;

public class Test {
  public static void main(String[] args) {
    Dog dog1 = new Dog();
    com.xiaoming.Dog dog2 = new com.xiaoming.Dog();
  }
}

包的命名
只能包含数字、字母、下划线、点,数字不能开头,不能是关键字保留字。
一般是小写字母和点组成,com.公司名.项目名.业务模块名

常用的包

  • java.lang.* 基本包,默认引入,不需手动引入;
  • java.until.* 工具包
  • java.net.* 网络包
  • java.awt.* GUI界面开发
import java.util.Scanner; //只引入Scanner类
import java.util.*; //引入java.util包下的所有类

建议只导入所使用的包,不导入全部包。

package用来声明当前类所在包,放在类的最上面,一个类中最多只有一句package。
import语句在package下面,在类定义的上面,没有顺序和数量要求。

2.2 访问修饰符

四种访问修饰符,用于控制方法和属性的访问权限。

  1. 公开级别:public 对外公开;
  2. 受保护级别:protected 对子类和同一个包中的类公开;
  3. 默认级别:无修饰符,对同一个包中的类公开;
  4. 私有级别:private 只有类内部可以访问,不对外公开。
访问级别 访问修饰符 同类 同包 子类 不同包
公开 public
受保护 protected
默认
私有 private

修饰符用来修饰类中的属性,成员方法和类;
只有默认和public才能修饰类;
成员方法的访问规则同属性一样。

2.3 封装

面向对象编程的三大特征:封装、继承、多态。
封装(encapsulation)就是把抽线出来的数据(属性)和对属性的操作(方法)封装在一起,数据被保护在内部,程序的其他部分只有通过被授权的操作(方法)才能使用数据。

封装的好处:隐藏实现细节;可以对数据进行验证,保证安全合理。

封装实现步骤:

  1. 将属性私有化private,使外部不能直接修改属性;
  2. 提供一个公共set方法,用于对属性判断并赋值;

public void setXXX(Type param){
//加入数据验证逻辑
this.name=name;
}

  1. 提供一个公共get方法,用来获取属性值;

public Type getXXX() {
//权限判断
return xxx;
}

public class Account {
    private String name;
    private double balance;
    private String password;

    //提供两个构造器
    public Account() {}

    public Account(String name, double balance, String password) {
        this.setName(name);
        this.setBalance(balance);
        this.setPassword(password);
    }

    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        if (name.length() >= 2 && name.length() <= 4) {
            this.name = name;
        } else {
            System.out.println("姓名长度必须2-4位");
            this.name = "anonymous";
        }
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        if (balance > 20) {
            this.balance = balance;
        } else {
            System.out.println("余额必须大于20");
        }
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        if (password.length() == 6) {
            this.password = password;
        } else {
            System.out.println("密码必须6位");
            this.password = "000000";
        }
    }
}

为了在创建对象时对初始化属性进行验证,可将set方法写在构造器中。

public Person(String name, int age, double salary) {
    this.setName(name);
    this.setAge(age);
    this.setSalary(salary);
}

2.4 继承

继承可以解决代码复用,当多个类存在相同属性和方法时,可以从中抽象出父类,在父类中定义这些属性和方法,所有的子类不需要重新定义,只需要通过extends声明继承自父类即可。
继承语法:

class 子类 extends 父类 {
}

子类会自动拥有父类定义的属性和方法;父类又叫超类、基类;子类又叫派生类。

继承细节:

  1. 子类继承父类所有属性和方法,但私有属性和方法不能在子类中直接访问,要通过父类提供的公共方法访问。
  2. 子类必须调用父类的构造器,完成父类的初始化。
  3. 当创建子类对象时,不管使用子类的哪个构造器,默认都会调用父类无参构造器;如果父类没有提供无参构造器,则必须在子类构造器中使用super去指定使用父类的哪个构造器完成对父类的初始化,否则编译不通过。
public class Base {
    /*
    public Base() {
        System.out.println("父类Base()构造器被调用");
    }
    */
    public Base(String name, int age) {
        System.out.println("父类Base(String name, int age)构造器被调用");
    }
}

public class Sub extends Base {
    public Sub() {
    	super("smith", 10);
    	System.out.println("子类Sub()构造器被调用");
    }
    public Sub(String name) {
        super("tom", 20);
        System.out.println("子类Sub(String name)构造器被调用");
    }
}
  1. 如果希望指定调用父类的某个构造器,则显式调用:super(参数列表),否则默认执行父类无参构造器,即super();
  2. super在使用时,必须放在构造器第一行(super只能在构造器中使用)。
  3. super()this()都只能放在构造器第一行,因此两者不能共存于同一个构造器中。
  4. Java所有类都是Object类的子类,,Object类是所有类的父类。
  5. 父类构造器的调用不限于直接父类,将一直向上追溯到Object类。
  6. 子类最多只能继承一个父类(直接继承),单继承机制。
  7. 不能滥用继承,子类与父类必须满足is-a逻辑关系(Student is a Person)。

2.5 super

super代表父类的引用,用于访问父类的属性、方法和构造器。

  • 可以访问父类的非私有属性和方法;
  • 当子类中有和父类成员重名时,必须通过super访问父类成员;如果没有重名,使用super、this和直接访问效果相同;
  • super的访问不限于直接父类,如果爷爷类和本类有重名的成员,也可以使用super访问爷爷类的成员;如果多个父类(上级类)中都有重名成员,super访问遵循就近原则;

super与this比较

区别点 this super
访问属性 访问本类中的属性,如果本类无此属性则向上继续查找 访问父类属性,父类无此属性则向上继续查找
调用方法 访问本类中的方法,如果本类无此方法则向上继续查找 访问父类方法,父类无此属性则向上继续查找
调用构造器 调用本类构造器,必须放在构造器首行 调用父类构造器,必须放在子类构造器首行
特殊 表示当前对象 子类中访问的父类对象

2.6 方法重写(override)

重写/覆盖:子类有一个方法,和父类的某个方法名称、返回类型、参数列表都相同,成为子类方法覆盖父类方法。

public class Animal {
    public void cry() {
    	System.out.println("动物叫");
    }
}

public class Dog extends Animal {
    public void cry() {
        System.out.println("狗叫");
    }
}
  • 子类方法的参数、方法名要和父类方法相同;
  • 子类方法的返回类型和父类方法的返回类型一样,或是父类方法返回类型的子类(比如父类方法返回类型Object,子类方法返回类型是String);
  • 子类方法不能缩小父类方法的访问权限;
名称 发生范围 方法名 形参列表 返回类型 修饰符
重载 本类 必须一样 类型、个数、顺序至少一个不同 无要求 无要求
重写 父子类 必须一样 相同 子类方法返回类型与父类一致或是父类方法返回类型的子类 子类方法不能缩小父类方法访问范围

2.7 多态

方法或对象具有多种形态,多态建立在封装和继承之上。

方法的多态:重写和重载体现多态。

对象的多态:

  1. 一个对象的编译类型和运行类型可以不一致;
  2. 编译类型在定义对象时就确定了,不能改变;
  3. 运行类型可以改变;
  4. 编译类型看定义是“=”左边,运行类型看“=”右边。
//animal编译类型为Animal,运行类型为Dog
Animal animal = new Dog();
//animal运行类型改为Cat
animal = new Cat();
public class Master {
    //animal编译类型为Animal,可以接收Animal子类的对象
    //food编译类型为Food,可以接收Food子类的对象
    public void feed(Animal animal, Food food) {
        Sys.out.println("给" + animal.getName() + "喂食" + food.getName());
    }
}

多态的前提:两个对象(类)存在继承关系。

向上转型

多态的向上转型:父类的引用指向了子类的对象,父类类型 引用名 = new 子类类型();,可以调用父类的所有成员,但不能调用特有的成员(在编译阶段,能调用哪些成员是由编译类型决定),最终运行效果看子类[运行类型]的具体实现(调用方法时,先从子类[运行类型]开始查找)。

向下转型

多态的向下转型:子类类型 引用名 = (子类类型) 父类引用,只能强转父类的引用,不能强转父类的对象;要求父类的引用必须是指向当前目标类型的对象;向下转型后可以调用子类类型的所有成员。

属性无重写,属性的值看编译类型。
instanceof操作符用于判断对象的运行类型是否为XX类型或其子类型。

动态绑定机制

动态绑定又称后期绑定,即在运行时根据具体对象的类型进行绑定。

  1. 当调用对象方法时,该方法会和该对象的内存地址/运行类型绑定;
  2. 当调用对象属性时,无动态绑定机制,哪里声明,哪里使用。

静态绑定又称前期绑定,程序执行前方法就已经被绑定,Java中的方法只有final、static、private和构造方法是前期绑定。

多态数组

数组的定义类型为父类类型,里面保存的实际元素类型为子类类型。

public static void main(String[] args) {
    //Person Student Teacher都有say()方法
    Person[] persons = new Person[5];
    persons[0] = new Person("adam", 18);
    persons[1] = new Student("adam", 18, 99);
    persons[2] = new Student("bob", 19, 70.1);
    persons[3] = new Teacher("chuck", 30, 3000);
    persons[4] = new Teacher("dick", 29, 3100);

    for (int i = 0; i < persons.length; i++) {
        //persons[i]编译类型为Person,运行类型根据实际情况由JVM判断
        System.out.println(persons[i].say()); //动态绑定机制
        
        //teach()是Teacher类特有方法,study()是Student类特有方法
        if (persons[i] instanceof Student) {
            ((Student) persons[i]).stduy(); //向下转型
        } else if (persons[i] instanceof Teacher) {
            ((Teacher) persons[i]).teach(); //向下转型
        } else {
            System.out.println("类型有误");
        }
    }
}

多态参数

方法定义的形参类型为父类类型,实参类型允许为子类类型。

public class PloyTest {
    public static void main(String[] args) {
        //父类Employee,子类Worker和Manager继承Employee
        Worker frank = new Worker("Frank", 1000);
        Manager gordon = new Manager("Gordon", 20000, 2000);
        PloyTest ployTest = new PloyTest();
        ployTest.showEmpAnnual(frank);
        ployTest.showEmpAnnual(gordon);

        ployTest.testWork(frank);
        ployTest.testWork(gordon);
    }

    public void showEmpAnnual(Employee e) {
        System.out.println(e.getAnnual());
    }

    public void testWork(Employee e) {
        if (e instanceof Worker) {
            //work()为Worker类专有方法
            ((Worker) e).work(); //向下转型
        } else if (e instanceof Manager) {
            //manage()为Manager类专有方法
            ((Manager) e).manage(); //向下转型
        } else {
            System.out.println("无操作");
        }
    }
}

2.8 Object类

equals()方法

==与equals对比:
==是比较运算符,既可以判断基本类型,也可以判断引用类型,对于基本类型判断值是否相等,对于引用类型判断地址是否相同(即判定是否为同一对象)。
equals是Object类的方法,只能判断引用类型。默认判断地址是否相等,子类中往往重写该方法,用于判断内容是否相等,比如Integer等。

public class Equals01 {
    public static void main(String[] args) {
        A a = new A();
        A b = a;
        A c = b;
        System.out.println(a == c); //true
        System.out.println(b == c); //true
        B bObj = a;
        System.out.println(bObj == c); //true
        
        Integer int1 = new Integer(1000);
        Integer int2 = new Integer(1000);
        System.out.println(int1 == int2); //false
        System.out.println(int1.equals(int2)); //true
    }
}

class B {}
class A extends B {}

重写equals方法:

class Person {
    private String name;
    private int age;
    private char gender;
    
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        //类型判断
        if (obj instanceof Person) {
            Person p = (Person)obj; //向下转型
            return this.name.equals(p.name) && this.age == p.age && this.gender == p.gender;
        }
        return false;
    }
}

hashCode()方法

返回该对象的哈希码值。
作用:

  • 提高具有哈希结构的容器的效率;
  • 两个引用,如果指向的是同一个对象,则哈希值一样;
  • 哈希值主要根据地址计算得来,不能完全等同于地址

toString()方法

默认返回:全类名@哈希值的十六进制。直接输出一个对象时,toString方法会被默认调用。
子类往往重写toString方法,用于返回对象属性信息。

finalize()方法

当对象被回收时,系统自动调用该对象的此方法。
子类可以重写该方法,做一些释放资源的操作。
当某个对象没有任何引用时,会被jvm认定为垃圾对象,通过垃圾回收机制销毁该对象,且在对象被销毁前调用finalize方法。
垃圾回收机制的调用是由系统决定,可以通过System.gc()主动触发垃圾回收机制。

3. 面向对象(高级)

3.1 类变量和类方法

类变量(静态变量)

静态变量被同一个类的所有对象共享,且在类加载的时候就生成了。
定义:访问修饰符 static 数据类型 变量名;
访问类变量:类名.类变量名对象名.类变量名
注意

  • 什么时候需要类变量:当需要让某个类的所有的对象都共享一个变量时;
  • 类变量与实例变量区别:类变量是该类所有对象共享,实例变量是每个对象共享;
  • 类变量的生命周期随类的加载开始,随着类的销毁而结束。

类方法(静态方法)

定义:访问修饰符 static 数据返回类型 方法名(){ }
调用:类名.类方法名对象名.类方法名
类方法的使用场景:当方法中不涉及到任何与对象相关的成员,可以设计成静态方法,比如工具类的方法。
注意

  • 类方法和普通方法都是随着类的加载而加载,将结构信息存储在方法区,类方法中无this参数,普通方法隐含着this参数;
  • 类方法中不允许使用和对象有关的关键字,如this和super;
  • 类方法中只能访问静态方法或静态变量;
  • 普通成员方法,既可以访问普通成员,也可以访问静态成员。

3.2 main方法

public static void main(String[] args){ }

  1. main方法是由JVM调用,因此该方法为public;
  2. JVM在执行main方法时不需要创建对象,因此必须是static;
  3. 该方法接收String数组,该数组保存执行Java命令时传递给所运行的类的参数:

$ java XXobj param1 param2

在main方法中,可以直接调用main方法所在类的静态方法或静态属性;但不能直接访问该类的非静态成员,必须先创建该类的实例对象后,才能通过该对象去访问类中非静态成员。

3.3 代码块

又称初始化块,属于类中的成员,类似于方法,将逻辑语句封装在方法体中,通过{}包围起来。(只有方法体的方法

[修饰符]{
    代码
};

修饰符可选,只能写static;
代码块分为两类:有static修饰的叫静态代码块,没有static修饰的叫普通代码块/非静态代码块;
;可以省略。

代码块相当于另一种形式的构造器(对构造器的补充),可以做初始化操作,例如多个构造器中都有重复语句,可以抽取到初始化块中,且先于构造器执行。

注意

  1. 静态代码块作用是对类进行初始化,随着类的加载而执行,只会执行一次;普通代码块每创建一个对象就执行。
  2. 类什么时候被加载:创建对象实例时;创建子类对象实例,父类也会被加载;使用类的静态成员时。
  3. 普通代码块在创建对象实例时,会被隐式调用,被创建一次就会调用一次,如果只是使用类的静态成员时,普通代码块不会执行。
  4. 创建一个对象时,在一个类中的调用顺序:
    1. 调用静态代码块和静态属性初始化(静态代码块和静态属性初始化调用的优先级一致,如果有多个,则按照定义顺序执行);
    2. 调用普通代码块和普通属性初始化(普通代码块和普通属性初始化调用的优先级一致,如果有多个,按照定义顺序执行);
    3. 调用构造器。
  5. 构造器的最前面隐含了super()和调用普通代码块。
  6. 创建一个子类对象时的调用顺序:
    1. 父类的静态代码块和静态属性(优先级一样,按定义顺序执行);
    2. 子类的静态代码块和静态属性(优先级一样,按定义顺序执行);
    3. 父类的普通代码块和普通属性初始化(优先级一样,按定义顺序执行);
    4. 父类的构造器;
    5. 子类的普通代码块和普通属性初始化(优先级一样,按定义顺序执行);
    6. 子类的构造器。
  7. 静态代码块只能直接调用静态成员,普通代码块可以调用任意成员。

3.4 单例设计模式

  • 所谓类的单例模式,就是采用一定的方法保证在整个软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法;
  • 有两种方式:饿汉式,懒汉式。

饿汉式

步骤:

  1. 构造器私有化,防止直接new;
  2. 在类的内部创建对象,静态的;
  3. 向外暴露一个静态的公共方法,getInstance()
  4. 类加载时,对象就已经创建。
public class Single01 {
    public static void main(String[] args) {
        GirlFriend gf = GirlFriend.getInstance();
        System.out.println(gf);
        GirlFriend gf2 = GirlFriend.getInstance();
        System.out.println(gf2);
        System.out.println(gf == gf2); //true
    }
}

class GirlFriend {
    private String name;

    private static GirlFriend gf = new GirlFriend("小红");
    private GirlFriend(String name) {
        this.name = name;
    }

    public static GirlFriend getInstance() {
        return gf;
    }
}

懒汉式

步骤:

  1. 构造器私有化;
  2. 定义一个static对象;
  3. 提供一个public的static方法getInstance(),可以返回一个对象;
  4. 只有当用户调用getInstance时,才返回对象。
public class Single02 {
    public static void main(String[] args) {
        Cat cat = Cat.getInstance();
        Cat cat1 = Cat.getInstance();
        System.out.println(cat == cat1); //true
    }
}

class Cat {
    private String name;
    private static Cat cat;

    private Cat(String name) {
        this.name = name;
    }

    public static Cat getInstance() {
        if (cat == null) {
            cat = new Cat("haha");
        }
        return cat;
    }
}

饿汉式VS懒汉式

  1. 二者最主要区别在于创建对象时机不同:饿汉式在类加载时就创建对象,懒汉式在使用时创建;
  2. 饿汉式不存在线程安全问题,懒汉式存在线程安全问题;
  3. 饿汉式存在浪费资源的可能,如果程序没有使用到对象实例,饿汉式创建的对象就浪费了,懒汉式不存在该问题;
  4. java.lang.Runtime就是单例模式。

3.5 final

final可以修饰类、属性、方法和局部变量。
final的使用需求:

  1. 当不希望类被继承时;
  2. 当不希望父类的某个方法被子类重写时;
  3. 当不希望类的某个属性值被修改时;
  4. 当不希望某个局部变量被修改时,可用final修饰(局部常量);
public class Final01 {
    public static void main(String[] args) {
        E e = new E();
        e.TAX_RATE = 0.2; //wrong
    }
}

final class A {}
class B extends A {} //wrong
// ---
class C {
    public final void hi() {}
}
class D extends C {
    public void hi() {} //wrong
}
// ---
class E {
    public final double TAX_RATE = 0.1;
}
// ---
class F {
    public void cry() {
        final double NUM = 0.01;
        NUM = 0.5; //wrong
    }
}

注意

  1. final修饰的属性又叫常量;
  2. final修饰的属性在定义时,必须赋初值,且以后无法修改,赋值可以在定义时、构造器中或代码块中;
  3. 如果final修饰的属性是静态的,则初始化的位置只能在定义时和静态代码块中;
  4. final类不能继承,但可以实例化对象;
  5. 如果类不是final类,但含有final方法,则该方法虽不能重写,但可以被继承;
public class Final02 {
    public static void main(String[] args) {
        CC cc = new CC();
        
        new EE().cal();
    }
}

class AA {
    public final double TAX_RATE = 0.01;
    public final double TAX_RATE2;
    public final double TAX_RATE3;
    public static final double REPO_RATE = 0.1;
    public static final double REPO_RATE2;
    
    public AA() {
        TAX_RATE2 = 0.02;
    }
    {
        TAX_RATE3 = 0.03;
    }
    static {
        REPO_RATE2 = 0.2;
    }
}
// ---
final class CC {}
// ---
class DD {
    public fianl void cal() {
        System.out.println("cal()");
    }
}

class EE extends DD { }
  1. 如果一个类已经被final修饰,就没有必要再用final修饰方法;
  2. final不能修饰构造器;
  3. final和static往往搭配使用效率更高,不会导致类加载(底层编译器做了优化);
  4. 一些包装类(Integer、Double、Float、Boolean、String等)都是final。

3.6 抽象类

当父类的某些方法需要声明,但是又不确定如何实现时,可以将其声明为抽象方法,则该类就是抽象类,访问修饰符 abstract 类名 {}
抽象方法没有方法体,访问修饰符 abstract 返回类型 方法名(参数列表);
当一个类中存在抽象方法时,需要将该类也声明为abstract。
一般地,抽象类会被继承,由其子类来实现。

abstract class Animal {
    private String name;

    public abstract void eat();
    
    public void cry() {
        System.out.println("cry");
    }
}

注意

  1. 抽象类不能被实例化;
  2. 抽象类不一定要包含抽象方法,也可以有实现的方法;
  3. abstract只能修饰类和方法;
  4. 抽象类可以有任意成员,比如非抽象方法、构造器、静态属性等;
  5. 抽象方法不能有主体,即{ }
  6. 如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法(即有方法体),除非它自己也声明为抽象类;
  7. 抽象方法不能使用private、final、static修饰,因为这些关键字与重写相违背。

3.7 模板设计模式

抽象类作为多个子类的模板,子类在抽象类的基础上进行扩展、改造,但子类总体上会保留抽象类的行为方式。
模板设计模式能解决的问题:

  • 当功能内部一部分实现是确定的,另一部分不确定,可以把不确定的部分暴露出去让子类实现;
  • 编写一个抽象父类,父类提供了多个子类的通用方法,并把一个或多个方法留给其子类实现。
abstract public class Template {
    public abstract void job();

    public void calculateTime() {
        long start = System.currentTimeMillis();
        job();
        long end = System.currentTimeMillis();
        System.out.println("执行时间:" + (end-start));
    }
}

3.8 接口

接口就是给出一些没有实现的方法,到某个类要使用的时候,再根据具体情况将这些方法实现出来。

package com.hspedu.Interf_;

public interface Usb {
    //规定接口的相关方法
    public void start();

    public void stop();
}
package com.hspedu.Interf_;

//Phone类实现Usb接口
public class Phone implements Usb{
    @Override
    public void start() {
        System.out.println("手机Usb连接");
    }

    @Override
    public void stop() {
        System.out.println("手机Usb断开");
    }
}
package com.hspedu.Interf_;
//Camera类实现Usb接口
public class Camera implements Usb{
    @Override
    public void start() {
        System.out.println("相机Usb连接");
    }

    @Override
    public void stop() {
        System.out.println("相机Usb断开");
    }
}
package com.hspedu.Interf_;

public class Computer {
    public void work(Usb usbInterface) {
        usbInterface.start();
        usbInterface.stop();
    }
}
package com.hspedu.Interf_;

public class Interface01 {
    public static void main(String[] args) {
        //创建手机、相机对象
        Phone phone = new Phone();
        Camera camera = new Camera();
        //创建计算机
        Computer computer = new Computer();
        computer.work(phone); //手机接入计算机
        System.out.println("----");
        computer.work(camera);
    }
}
  • 在JDK7前,接口里的所有方法都没有方法体,即都是抽象方法;
  • JDK8之后接口可以有静态方法,默认方法(default关键字修饰),即接口中可以有方法实现。

注意

  1. 接口不能被实例化;
  2. 接口中所有方法是public,接口中抽象方法可以不用abstract修饰;
  3. 一个普通类要实现接口,就必须把接口中所有方法实现;
  4. 抽象类实现接口,可以不用实现接口方法;
  5. 一个类可以同时实现多个接口;
interface IB {
    void hi();
}
interface IC {
    void say();
}

class Pig implements IB, IC {
    @Override
    public void hi() {
        
    }
    @Override
    public void say() {

    }
}
  1. 接口中的属性,只能是final,而且是public static final;
  2. 接口中属性的访问形式:接口名.属性名
  3. 接口不能继承其他类,但可以继承多个其他接口interface A extends B, C{}
  4. 接口的修饰符只能说public和默认,和类的修饰符一样。

继承vs实现接口

当子类继承了父类,就自动拥有了父类的功能;如果子类需要扩展功能,可以通过实现接口。实现接口是对Java单继承机制的一种补充。

  • 继承的价值在于解决代码复用性和可维护性,接口的价值在于设计好各种规范方法,让其他类实现这些方法;
  • 接口比继承更加灵活,继承是满足is-a关系,接口只需满足like-a关系;
  • 接口在一定程度上实现代码解耦(接口规范性+动态绑定)。

接口的多态特性

  1. 多态参数:接口引用可以指向实现了接口的类的对象
public class Interface02 {
    public static void main(String[] args) {
        IF if01 = new Monster();
        if01 = new Car(); //
    }
}

interface IF {}
class Monster implements IF {}
class Car implements IF {}
  1. 多态数组

  2. 多态传递

public class Interface03 {
    public static void main(String[] args) {
        //接口类型的变量可以指向实现该接口的类的对象实例
        IG ig = new Teacher();
        //IG继承IH,Teacher实现IG,也相当于实现了IH
        IH ih = new Teacher();
    }
}

interface IH {
    void hi();
}
interface IG extends IH {}
class Teacher implements IG {
    @Override
    public void hi() {
        
    }
}

3.9 内部类

一个类的内部又完整地嵌套了另一个类,被嵌套的类成为内部类。
内部类最大特点是可以直接访问私有属性,并且可以体现类与类之间的包含关系。

四种内部类

定义在外部类局部位置上(比如方法内):

  • 局部内部类(有类名)
  • 匿名内部类(无类名)

定义在外部类的成员位置上:

  • 成员内部类(无static修饰)
  • 静态内部类(有static修饰)

局部内部类

局部内部类定义在外部类的局部位置。

  1. 可以直接访问外部类的所有成员,包括私有的;
  2. 不能添加访问修饰符,因为它的低位就是一个局部变量,局部变量不能使用修饰符,但可以用final修饰;
  3. 作用域:仅在定义它的方法或代码块中;
  4. 如何使用内部类:在定义内部类的外部类方法中,先创建该内部类对象,再访问(即在作用域内使用);
  5. 外部其他类不能访问局部内部类;
  6. 如果外部类成员和局部内部类成员重名时,默认遵循就近原则,如果想访问外部类成员,则使用外部类名.this.成员访问。

匿名内部类

匿名内部类定义在外部类的局部位置,比如方法中,且没有类名。

new 类或接口(参数列表) {
    类体
};
public class AnonymousInnerClass {
    public static void main(String[] args) {
        Outer04 outer04 = new Outer04();
        outer04.method();
    }
}

class Outer04 {
    private int n1 = 10;
    public void method() {
        IA tiger = new IA() {
            @Override
            public void cry() {
                System.out.println("wowow");
            }
        };
        tiger.cry();
    }
}

interface IA {
    public void cry();
}

注意

  1. 匿名内部类即时一个类的定义,也是一个对象。
Person p = new Person() {
    @Override
    public void hi(){
        System.out.println("匿名内部类重写hi方法");
    }
};
p.hi();
//----
new Person() {
    @Override
    public void hi(){
        System.out.println("匿名内部类重写hi方法");
    }
}.hi(); //直接调用
  1. 可以直接访问外部类的所有成员,包含私有的;
  2. 不能添加访问修饰符,因为它的地位就是一个局部变量;
  3. 作用域:仅仅在定义它的方法或代码块中;
  4. 外部其他类不能访问匿名内部类;
  5. 如果外部类成员和匿名内部类成员重名,匿名内部类访问默认遵循就近原则,如果想访问外部类成员,则使用外部类名.this.成员

成员内部类

成员内部类定义在外部类的成员位置,且没有static修饰。

public class MemberInnerClass {
    public static void main(String[] args) {
        Outer08 outer08 = new Outer08();
        outer08.t1();
    }
}

class Outer08 {
    private int n1 = 10;
    public String name = "张三";

    class Inner08 {
        public void say() {
            System.out.println("n1=" + n1 + " name=" + name);
        }
    }

    public void t1() {
        Inner08 inner08 = new Inner08();
        inner08.say();
    }
}

注意

  1. 可以直接访问外部类的所有成员,包含私有的;
  2. 可以添加任意访问修饰符,因为它的低位是成员;
  3. 作用域:和外部类的其他成员一样;
  4. 成员内部类访问外部类成员,直接访问;
  5. 外部类访问内部类,先创建成员内部类对象再访问;
  6. 外部其他类也可以访问成员内部类
public class MemberInnerClass {
    public static void main(String[] args) {
        //方式一
        Outer08 outer08 = new Outer08();
        Outer08.Inner08 inner08 = outer08.new Inner08();
        inner08.say();
        //方式二,在外部类中编写一个方法可以返回Inner08对象
        Outer08.Inner08 inner08Instance = outer08.getInner08Instance();
        inner08Instance.say();
        //方式三
        Inner08 inner08_ = new Outer08().new Inner08();
    }
}
class Outer08 {
    class Inner08 {
        public void say() {
            System.out.println("n1=" + n1 + " name=" + name);
        }
    }
    public Inner08 getInner08Instance() {
        return new Inner08();
}
  1. 如果外部类成员与内部类成员重名,内部类访问遵循就近原则,如果想访问外部类成员,则使用外部类名.this.成员

静态内部类

定义在外部类的成员位置,且有static修饰。
注意

  1. 可以直接访问外部类的所有静态成员,包含私有的,但不能访问非静态成员;
  2. 可以添加任意访问修饰符,因为它的低位就是一个成员;
  3. 作用域:为整个类体;
  4. 静态内部类访问外部类静态成员,直接访问;
  5. 外部类访问静态内部类,先创建对象再访问;
  6. 外部其他类访问静态内部类:
//方式一
Outer10.Inner10 inner10 = new Outer10.Inner10();
inner10.say();
//方式二,编写一个返回静态内部类对象实例的方法
Outer10.Inner10 inner101 = outer10.getInstance10();

  1. 如果外部类成员和静态内部类成员重名时,静态内部类访问时,默认遵循就近原则,如果想访问外部类成员,使用外部类名.成员访问。
posted @ 2022-03-20 17:37  KRDecad3  阅读(14)  评论(0编辑  收藏  举报