[TOC]

Java面向对象初级

一、方法的重载

  1. 注意事项和使用细节

    • 方法名必须相同;
    • 形参列表必须不同(形参类型、个数或顺序至少有一个不同,参数名无要求);
    • 返回类型无要求。
  2. 可变参数public int sum(int ... nums) 表示可接收多个参数。

    • 可变参数本质为数组;

    • 参数为0个或多个;

    • 实参可以为数组;

    • 可以与普通参数放在同一形参列表,但必须放在最后

    • 一个形参列表中只能有一个可变参数。

二、作用域

  1. 全局变量有默认值,可以不用赋值,局部变量无默认值,需要赋值。
  2. 属性可以与局部变重名,使用时遵循就近一致原则。

三、构造器

  1. 构造器是初始化对象,并非创建对象。

  2. 一个类可以定义多个不同的构造器,即构造器重载。

  3. 对象创建流程:

    • 对于以下代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      class Person{ //默认初始化
      int age = 90; //显式初始化
      String name;
      // 构造器
      Person(String n,int a){ //构造器初始化
      name = n;
      age = a;
      }
      }
      Preson p1 = new Person("小倩",20);
    • 加载Person类的信息(Person.class),只加载一次;
    • 在堆中分配空间(地址);

      • 完成对象初始化:

      • 默认初始化(age=0,name=null)

      • 显式初始化(age=90,name=null)

      • 构造器初始化(age=20,name=小倩)

    • 把对象在堆中的地址返回给p1(引用)

四、封装

封装的作用

  1. 提高代码的安全性;
  2. 提高代码的复用性;
  3. “高内聚”:封装细节,便于修改内部代码,提高可维护性;
  4. “低耦合”:简化外部调用,便于调用者使用,便于扩展和写作。

封装主要代码

  1. get()set()函数,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package demo;

import java.util.Scanner;

public class test1 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
person p = new person();
p.setName("小明");
p.setAge(600);
System.out.println(p.getName());
System.out.println(p.getAge());
}
}
class person {
private String name;
private int age;

public String getName() {
return name;
}
public void setName(String name) {
//数据校验
if(name.length() < 2 || name.length() > 6)
{
System.out.println("名字长度不符合要求,默认张三!");
this.name = "张三";
}
else {
this.name = name;
}
}
public int getAge() {
return age;
}
public void setAge(int age) {
//数据校验
if(age > 120)
{
System.out.println("年龄不符合要求,默认18!");
this.age = 18;
}
else {
this.age = age;
}
}
}

封装与构造器

  1. 构造器可破解封装中的数据校验,所以可进行下列操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package demo;

import java.util.Scanner;

public class test1 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
person p = new person();
p.setName("小明");
p.setAge(600);
System.out.println(p.getName());
System.out.println(p.getAge());
System.out.println("**********************");
person p1 = new person("小明",400);
System.out.println(p1.getName());
System.out.println(p1.getAge());
}
}
class person {
private String name;
private int age;

public person() {
}

public person(String name, int age) {
//破解了数据校验
this.name = name;
this.age = age;
//修复(构造器中调用set函数)
setName(name);
setAge(age);
}

public String getName() {
return name;
}
public void setName(String name) {
//数据校验
if(name.length() < 2 || name.length() > 6)
{
System.out.println("名字长度不符合要求,默认张三!");
this.name = "张三";
}
else {
this.name = name;
}
}
public int getAge() {
return age;
}
public void setAge(int age) {
//数据校验
if(age > 120)
{
System.out.println("年龄不符合要求,默认18!");
this.age = 18;
}
else {
this.age = age;
}
}
}

五、继承

  1. 创建子类对象时,不论使用子类的那个构造器,默认情况下都会调用父类的无参构造器,若父类没有提供无参构造器,则必须在子类的构造器中用super去指定父类中的构造器(如super(String name,int age);)以完成父类的初始化,否则编译不通过。

  2. 若希望指定去调用父类的某个构造器时,需要显式的调用一下:super(参数列表)

  3. super()在使用时,必须放在构造器第一行(this()也是),故两方法不能共存于同一个构造器中。

  4. super()this()的区别:

    • super()访问父类中和子类重名的属性,若不重名,则和this()功能一致。

    • super()直接访问父类中的属性(方法),this()先在本类中找属性(方法),再去父类中找属性(方法)。

  5. 方法重载overload和方法重载override的比较:

    • 重载发生在子类,方法名一样,形参类别、个数或顺序至少有一个不同,返回类型和修饰符无要求;
    • 重写发生在父子类,方法名一样,形参类别、个数或顺序都相同,子类重写的方法的返回类型和父类返回类型一致或是其子类,子类的权限不能小于父类。

六、多态

方法的多态

  • 方法的重载和重写。

对象的多态

  • 一个对象的编译类型("="左边)和运行类型("="右边)可以不一致。

多态的向上转型

  • 本质:父类的引用指向子类的对象;

  • 语法:父类类型 引用名 = new 子类类型();

  • 特点:编译看左边,运行看右边;

  • 不能调用子类特有的成员。

1
2
3
Animal animal = new Cat(); //向上转型
Object obj = new Cat();
animal.eat(); //根据就近原则先去Animal中找相应的方法

多态的向下转型

  • 语法:子类类型 引用名 = (子类类型) 父类引用;

  • 只能强转父类的引用,不能强制父类的对象(对象创建后不再改变);

  • 当向下转型后,可以调用子类类型中的所有成员;

  • 要求父类的当前引用必须指向的是当前目标类型的对象。

1
2
3
Animal animal = new Cat(); //此时animal是指向Cat类型的对象
Cat cat = (Cat) animal;//编译类型和运行类型都是Cat
Dog dog = (Dog) animal;//编译通过,运行报错,违反要求:要求父类的当前引用必须指向的是当前目标类型的对象。

属性的“重写”

  1. 属性无“重写”之说,属性只看编译类型,即“=”左边。
1
2
3
4
5
6
7
8
A a = new B();
System.out.print(a.age); // 10(编译类型为A,找A的属性)
class A{
int age = 10;
}
class B extend A{
int age = 20;
}
  1. instenceof比较操作符,用于判断对象的运行类型是否为xx类型或xx类型的子类型。
1
2
3
4
5
6
7
8
9
10
11
12
B b = new B();//运行类型为B
System.out.print(b instenceof B); //true
System.out.print(b instenceof A); //true

A a = new B();//运行类型为B
System.out.print(a instenceof B); //true
System.out.print(a instenceof A); //true

Object obj = new Object();//运行类型为Object
System.out.print(obj instenceof A); //false
class A{}
class B extend A{}

动态绑定机制

  • 当调用对象方法时,该方法会与该对象的内存地址/运行类型绑定;

    (即若调用的方法子类没有而父类有,则先找到父类方法执行,但此时在父类方法中又需要执行子类和父类都有的方法时,需要看该对象的运行类型是哪个类,就执行那个类中的方法。重名的方法和对象的运行类型动态绑定)

  • 当调用对象属性时,没有动态绑定机制,哪里声明,哪里使用。

多态数组

  • 调用共有方法
1
2
3
4
5
6
7
8
9
10
Person[] persons = new Person[3];//创建一个Person对象数组
persons[0] = new Person("Jack",20);
persons[1] = new Teacher("Jack",20);
persons[2] = new Student("Jack",20);
//循环遍历多态数组,调用say方法
for(int i = 0;i < persons.length;i++)
{
//此时需要注意动态绑定机制,编译类型都为Person,运行类型根据实际情况看
persons.say();
}
  • 调用特有方法(使用类型判断 + 向下转型)
1
2
3
4
5
6
7
8
9
10
11
12
13
Person[] persons = new Person[3];//创建一个Person对象数组
persons[0] = new Person("Jack",20);
persons[1] = new Teacher("Jack",20);
persons[2] = new Student("Jack",20);
//循环遍历多态数组,调用say方法
for(int i = 0;i < persons.length;i++)
{
if(persons[i] instanceof Student){
Student student = (Student)persons[i];//向下转型
student.study();
//((Student)persons[i]).study();
}
}

多态参数

  • 方法定义的形参参数类型为父类类型,实参类型可以为子类类型]
1
2
3
Student student = new Syudent();
getName(student); //实参为student(子类)
void getName(Person p){}//形参为Person(父类)

七、Object类详解

equals方法

  1. ==equals的对比

    • ==既可以判断基本类型,又可以判断引用类型;
    • ==若判断基本类型(如比较数值大小时),判断的是值是否相等;
    • ==若判断引用类型(如比较两对象时),判断的是地址是否相等,即判断是否为同一个对象;
    • equals是Object类中的方法,只能判断引用类型;
    • equals默认判断的是地址是否相等,子类中往往重写该方法,用于判断内容是否相等。
  2. 举例:Person类中equals方法的重写

    Person类中的equals重写,判断两对象地址或属性是否一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package demo;

public class test1 {
public static void main(String[] args) {
Person p1 = new Person("a",10);
//Person p2 = p1;
Person p2 = new Person("a",10);
System.out.println(p1.equals(p2));
}
}
class Person {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

//Person类中的equals重写,判断两对象地址或属性是否一样
public boolean equals(Object obj)
{
if (this == obj)
{
return true;
}
if(obj instanceof Person)
{
Person p = (Person) obj;
return p.name.equals(this.name) && p.age == this.age;
}
return false;
}
}

hashCode

  1. hashCode的几点小结
    • 提高具有哈希结构的容器的效率;
    • 两个引用,如果指向的时同一个对象,则哈希值肯定是一样的,否则不一样;
    • 哈希值主要根据地址号来来的,不能完全将哈希值等价于地址;
    • hashCode也会重写。

toString方法

  1. 默认返回:全类名+@+哈希值的16进制,子类往往重写toString方法,用于返回对象的属性信息;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package demo;

public class test1 {
public static void main(String[] args) {
Person p1 = new Person("a",10);
System.out.println(p1.toString()+" "+p1.hashCode());
//输出:demo.Person@49e4cb85 1239731077
}
}
class Person {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
  1. 重写toString方法,打印对象或者拼接对象时,都会自动调用该对象的toString形式;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
   package demo;

public class test1 {
public static void main(String[] args) {
Person p1 = new Person("a",10);
System.out.println(p1.toString()+" "+p1.hashCode());
//输出:Person{name='a', age=10} 1945604815
}
}
class Person {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}
//重写toString方法,输出对象的属性

@Override
public String toString() { //重写后,一般输出对象的属性值,当然也可以自己定制
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
  1. 当直接输出一个对象时,toString方法会被默认调用。(如:System.out.println(Person);就会默认调用Person.toString())
1
2
3
Person p1 = new Person();
System.out.println(p1);
//等价于System.out.println(p1.toString());

Java面向对象高级

一、 类变量和类方法

类变量(静态变量)

  1. 定义语法:访问修饰符 static 数据类型 变量名(推荐)或static 访问修饰符 数据类型 变量名

  2. 访问方法:类名.类变量名(推荐)或者对象名.类变量名

  3. 类变量是被类声明的所有对象共享的,实例对象是对象独有的;

  4. 类变量在类加载的时候生成,即对象未创建是也可使用;

  5. jdk8以前类变量存放在方法区,jdk8以后类变量放在里;

类方法(静态方法)

  1. 定义语法:访问修饰符 static 数据返回类型 方法名(){}(推荐)或static 访问修饰符 数据返回类型 方法名(){}

  2. 调用方法:类名.类方法名(推荐)或者对象名.类方法名

  3. 使用场景:将一些通用的方法设计成静态类,可以不创建对象就调用相关方法,如Math.sqrt()

  4. 类方法和普通方法都是随着类的加载而加载,将结构信息储存在方法区,但类方法中无this(),普通方法中隐含this()

  5. 类方法不允许使用和对象有关的关键字,如super()this(),普通方法可以;

  6. 静态方法只能访问静态成员,而普通成员方法,既可以访问普通变量(方法),也可以访问静态变量(方法)。

二、main方法语法

  1. 解释main方法的形式:public static void main(String[] args){}

    • main方法是虚拟机调用;
    • Java虚拟机需要调用类的main()方法,所以该方法的访问权限必须是public
    • Java虚拟机在执行main()方法时不必创建对象,所以方法必须是static
    • 该方法接收String类型的数组参数,该数组中保存执行Java命令时传递给所运行的类的参数,即java 执行的程序 参数1 参数2 参数3 ...

    有以下代码:

1
2
3
4
5
6
7
8
public class test1 {
public static void main(String[] args) {
for (int i = 0;i < args.length;i++)
{
System.out.println("第" + (i+1) + "个参数为:" + args[i]);
}
}
}

在命令行编译执行观察参数:

image-20220322115013733

三、代码块

  1. 代码块相当于另一种形式的构造器(对构造器的补充机制),可做初始化的操作;
  2. 代码块先于构造器加载;
  3. 静态代码块只在类加载时执行一次,普通代码块每创建一次对象就执行一次;
  4. 当只使用某个类的静态成员(未创建该类对象)时,该类的普通代码块不会被执行,但只要加载类时,静态代码块都会执行一次;
  5. 类什么时候加载:
    • 创建对象实例时(new)
    • 创建子类对象实例,父类会被加载,即先加载父类(及其代码块或构造器),再加载子类(及其代码块或构造器)
    • 使用类的静态成员时(静态属性、静态成员)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package demo;

public class test1 {
public static void main(String[] args) {
A a = new A(12);
System.out.println("=======================");
A b = new A("Hello");
}
}
class A
{
int a;
String b;
//静态代码块
static
{
System.out.println("静态代码块只执行一次!");
}
//普通代码块
{
System.out.println("输出公共代码。");
System.out.println("代码块无论放在哪个位置,都优先加载。");
System.out.println("普通代码块每创建一次对象就执行一次!");
};
//构造器
public A(int a) {
this.a = a;
System.out.println(a);
// System.out.println("输出公共代码。");
}

public A(String b) {
this.b = b;
System.out.println(b);
// System.out.println("输出公共代码。");
}
}

/*
输出:
静态代码块只执行一次!
输出公共代码。
代码块无论放在哪个位置,都优先加载。
普通代码块每创建一次对象就执行一次!
12
=======================
输出公共代码。
代码块无论放在哪个位置,都优先加载。
普通代码块每创建一次对象就执行一次!
Hello
*/

四、类中调用的顺序(重点补充)

  1. 在继承的类中,构造器的前面其实隐含了super()和普通代码块,故调用顺序为父类中的代码块->父类中的构造器->子类中的代码块->子类中的构造器,示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package demo;

public class test1 {
public static void main(String[] args) {
B b = new B();
}
}

class A //父类Object
{
//super()
//普通代码块
{
System.out.println("A的代码块被调用。");
}
//构造器
public A()
{
System.out.println("A的构造器被调用。");
}
}
class B extends A
{
//super() 即父类A
//普通代码块
{
System.out.println("B的普通代码块被调用。");
}
//构造器
public B()
{
System.out.println("B的构造器被调用。");
}
}

/*
输出:
A的代码块被调用。
A的构造器被调用。
B的普通代码块被调用。
B的构造器被调用。
*/
  1. 创建一个对象时,在一个类中调用的顺序是:

    • 调用静态代码块和静态属性初始化(两者优先级相同,若有多个,则按照定义顺序调用)
    • 普通代码块和普通属性的初始化(两者优先级相同,若有多个,则按照定义顺序调用)
    • 调用构造器方法
  2. 创建一个子类对象时(继承关系),调用顺序是:

    • 父类的静态代码块和静态属性(优先级一样,按定义顺序执行)
    • 子类的静态代码块和静态属性(优先级一样,按定义顺序执行)
    • 父类的普通代码块和普通属性(优先级一样,按定义顺序执行)
    • 父类的构造方法
    • 子类的普通代码块和普通属性(优先级一样,按定义顺序执行)
    • 子类的构造方法
  3. 补充:静态代码块只能调用静态成员(静态属性和静态方法),普通代码块可以调用任意成员(包括静态成员)。

五、单例设计模式

设计模式:是在大量的实践中总结和理论化后优选的代码结构、编程风格以及解决问题的思考方式。


单例设计模式:采取一定的方法保证在整个软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。

单例模式有两种方式:饿汉式和懒汉式(都要保证对象的唯一性)

饿汉式(线程安全)

  • 饿汉式在类加载时创建对象实例;
  • 饿汉式是不论是否要用某个对象,先将该对象创建好,故可能会出现多余对象,造成资源的浪费;
  • 饿汉式线程安全。

懒汉式(线程不安全)

  • 懒汉式在使用对象时才会创建对象实例;
  • 懒汉式是先定义唯一的对象,然后在调用对象时再创建对象,不会出现多余对象浪费资源的情况;
  • 懒汉式线程不安全。

演示代码SingleTon.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
package CodeBase.SingleTon;

//饿汉式
/*
步骤:
1.将构造器私有化
2.在类的内部直接创建对象(该对象是static)
3.提供一个公共的static方法,返回创建好的对象
*/
class SingleTon_1
{
String name;
//先创建一个私有的静态对象
private static SingleTon_1 singleTon_1 = new SingleTon_1("饿汉式");
//构造器私有
private SingleTon_1(String name) {
this.name = name;
}
//公共静态方法返回创建好的对象
public static SingleTon_1 getInstance()
{
return singleTon_1;
}
}
//懒汉式
/*
步骤:
1.构造器私有化
2.先定义一个static对象,但不创建
3.定义一个public的static方法,可以返回一个对象
*/
class SingleTon_2
{
String name;
//先定义一个对象
private static SingleTon_2 singleTon_2;
//构造器私有
private SingleTon_2(String name){
this.name = name;
};
//定义返回对象的方法
public static SingleTon_2 getInstance()
{
if(singleTon_2 == null)
{
singleTon_2 = new SingleTon_2("懒汉式");
}
return singleTon_2;
}
}
public class SingleTon
{
public static void main(String[] args) {
System.out.println("=============正常情况下============");
SingleTon_Test singleTon_test = new SingleTon_Test();
singleTon_test.singleTon1_test();
System.out.println("==========================");
singleTon_test.singleTon2_test();
}
}
//测试对象是否位唯一
class SingleTon_Test {
//测试饿汉式对象
void singleTon1_test()
{
SingleTon_1 test_singleTon_1_1 = SingleTon_1.getInstance();
SingleTon_1 test_singleTon_1_2 = SingleTon_1.getInstance();
System.out.println(test_singleTon_1_1.name);
System.out.println("饿汉式对象一地址:" + test_singleTon_1_1);
System.out.println("饿汉式对象二地址:" + test_singleTon_1_2);
}
//测试懒汉式对象
void singleTon2_test()
{
SingleTon_2 test_singleTon_2_1 = SingleTon_2.getInstance();
SingleTon_2 test_singleTon_2_2 = SingleTon_2.getInstance();
System.out.println(test_singleTon_2_1.name);
System.out.println("懒汉式对象一地址:" + test_singleTon_2_1);
System.out.println("懒汉式对象二地址:" + test_singleTon_2_2);
}
}
/*
输出:
饿汉式
饿汉式对象一地址:CodeBase.SingleTon.SingleTon_1@22f71333
饿汉式对象二地址:CodeBase.SingleTon.SingleTon_1@22f71333
==========================
懒汉式
懒汉式对象一地址:CodeBase.SingleTon.SingleTon_2@6aaa5eb0
懒汉式对象二地址:CodeBase.SingleTon.SingleTon_2@6aaa5eb0
*/

多线程情况下的饿汉式和懒汉式

未加线程锁演示代码Thread_SingleTon.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package CodeBase.SingleTon;

public class Thread_SingleTon {
public static void main(String[] args) {
System.out.println("=============多线程情况下============");
Thread_SingleTon_Test.Thread_Test_SingleTon_1();
Thread_SingleTon_Test.Thread_Test_SingleTon_2();
}
}

//多线程饿汉式
class Thread_SingleTon_1 implements Runnable
{
@Override
public void run() {
//让线程停一会方便观察
try {
Thread.sleep(20);
}
catch (Exception e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 饿汉式:"+ SingleTon_1.getInstance());
}
}
//多线程懒汉式
class Thread_SingleTon_2 implements Runnable
{
@Override
public void run() {
try {
Thread.sleep(20);
}
catch (Exception e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 懒汉式:" + SingleTon_2.getInstance());
}
}

class Thread_SingleTon_Test
{
//为饿汉式创建2个线程
public static void Thread_Test_SingleTon_1()
{
Thread one = new Thread(new Thread_SingleTon_1());
Thread two = new Thread(new Thread_SingleTon_1());
//开启线程
one.start();;
two.start();
}
//为懒汉式创建2个线程
public static void Thread_Test_SingleTon_2()
{
Thread one = new Thread(new Thread_SingleTon_2());
Thread two = new Thread(new Thread_SingleTon_2());
//开启线程
one.start();;
two.start();
}
}

/*
输出(一种情况):饿汉式为同一个对象,懒汉式为不同的对象
=============多线程情况下============
Thread-1 饿汉式:CodeBase.SingleTon.SingleTon_1@3f58200d
Thread-0 饿汉式:CodeBase.SingleTon.SingleTon_1@3f58200d
Thread-2 懒汉式:CodeBase.SingleTon.SingleTon_2@59355dd6
Thread-3 懒汉式:CodeBase.SingleTon.SingleTon_2@61034766
*/

多线程中懒汉式的改进

  • 为每个对象设置一个“互斥锁”,这表明,在每一个时刻只有一个线程持有该互斥锁,而其他线程若要获得该互斥锁,必须等到该线程(持有互斥锁的线程)将其释放。
  • 为了使用这个“互斥锁”,在JAVA语言中提供了synchronized关键字,这个关键字即可修饰函数,也可以修饰代码,实际上可以将其理解为就是一个锁,当一个线程执行该临界代码的时候,用synchronized给该线程先上锁,其它线程进不来,当线程代码执行完了的时候有释放该锁,只不过释放锁是隐式的不需要显示的指明,随代码的执行完毕,锁自动的被释放;
  • volatile关键字可以禁止指令重排。

演示代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package CodeBase.SingleTon;

public class Thread_SingleTon {
public static void main(String[] args) {
System.out.println("=============改进懒汉式==============");
Thread_SingleTon_Test.Test_Fix_Thread_SingleTon_2();
}
}
//多线程懒汉式改进
class Fix_SingleTon_2
{
String name;
//先定义一个对象
private volatile static Fix_SingleTon_2 fix_singleTon_2; //第二层锁,volatile关键字禁止指令重排
//构造器私有
private Fix_SingleTon_2(String name){
this.name = name;
};
//定义返回对象的方法
public static Fix_SingleTon_2 getInstance()
{
if(fix_singleTon_2 == null)//第一层检查,检查是否有引用指向对象,高并发情况下会有多个线程同时进入
{
synchronized (Fix_SingleTon_2.class)
{
//双重检查,防止多个线程同时进入第一层检查(因单例模式只允许存在一个对象,故在创建对象之前无引用指向对象,所有线程均可进入第一层检查)
//假设没有第二层检查,那么第一个线程创建完对象释放锁后,后面进入对象也会创建对象,会产生多个对象
if(fix_singleTon_2 == null)
{
fix_singleTon_2 = new Fix_SingleTon_2("多线程懒汉式");
}
}
}
return fix_singleTon_2;
}
}
//多线程改进懒汉式
class Fix_Thread_SingleTon_2 implements Runnable
{
@Override
public void run() {
try {
Thread.sleep(20);
}
catch (Exception e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 改进懒汉式:" + Fix_SingleTon_2.getInstance());
}
}
class Thread_SingleTon_Test
{
//为改进后的懒汉式创建2个线程
public static void Test_Fix_Thread_SingleTon_2()
{
Thread one = new Thread(new Fix_Thread_SingleTon_2());
Thread two = new Thread(new Fix_Thread_SingleTon_2());
//开启线程
one.start();;
two.start();
}
}
//此时对象唯一

六、final关键字

使用场景

  • 不希望类被继承时;
  • 不希望父类的某个方法被子类覆盖、重写时;
  • 不希望类中某个属性的值被修改时;
  • 不希望某个局部变量被修改时。

注意事项及细节讨论

  • final修饰的属性又叫常量,一般用XX_XX_XX命名;

  • final修饰的属性在定义是赋初值,并且不能再修改,赋值可以在下列位置:

    • 定义时:如public final double TAX_RATE=0.08
    • 在构造器中;
    • 在代码块中。
  • final修饰的属性时静态的,则初始化的位置只能在定义时和在静态代码块中,不能在构造器中赋值;

  • final类不能继承,但可以实例化对象;

  • 如类不是final类,但类里有final方法,则该方法不能被重写,但类可以被继承;

  • 一般来说,若一个类已经是final类,则不需要在此类中写final方法;

  • final不能修饰构造器;

  • final往往和static配合使用,效率更高,底层编译器做了优化:

    如下代码:

    • 直接访问static final定义的属性时,编译时就知道了a的值,所以直接访问不会初始化Final类,即static代码块不会被加载;
    • static final定义的属性需要某个方法来获取时,编译时无法知道b的值,只有运行时才能知道,所以在访问b的值时,需要先初始化类,即static代码块会被加载。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package demo;

public class test1 {
public static void main(String[] args) {
System.out.println("直接访问static final定义的属性:"+Final.a);
System.out.println("=======================");
System.out.println("访问static final定义的方法:"+Final.b);
}
}

class Final //父类Object
{
static final int a = 10;
static final int b = b();

static {
System.out.println("Final类被加载。");
}

public static int b()
{
return 20;
}
}

/*
输出:
直接访问static final定义的属性:10
=======================
Final类被加载。
访问static final定义的方法:20
*/
  • 包装类(Integer,Double,Float,Boolean)等都是final类,String也是final类。

七、抽象类和抽象方法

基本概述

  • 形式:abstract class A{}public abstract void B()
  • 当父类方法不确定时,考虑将该方法设计为抽象方法;
  • 所谓抽象方法就是没有实现的方法,即没有方法体;
  • 当一个类中存在抽象方法时,需要将该类声明为抽象方法;
  • 一般来说,抽象类会被继承,由子类实现其抽象方法。

使用细节

  • 抽象类不能被实例化;
  • 抽象类不一定包含抽象方法。包含抽象方法的类必须为抽象类;
  • abstract只能修饰方法和类;
  • 抽象类可以有任意成员,如非抽象方法、构造器、静态属性等;
  • 若一个类继承了抽象类,则它必须实现抽象类的所有抽象方法,除非它自己也声明为抽象类;
  • 抽象方法不能用private,final,static修饰,因为这些关键字都是和重写相违背的。

八、接口

基本介绍

  • 接口相当于提供了一个规范;

  • 接口就是给出一些没有实现的方法,封装到一起,到某个类要使用时,在实现这些方法。

1
2
3
4
5
6
7
8
9
10
interface 接口名{
//属性
//方法
}

class 类名 implements 接口{
//自己属性
//自己方法
//必须实现接口的抽象方法
}
  • 在jdk7.0以前,接口中所有方法都没有方法体,都是抽象方法;jdk8.0后,接口中可以有静态方法(static)和默认方法(default),即接口中可以有方法的具体实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface A{
//抽象方法可以省略abstract
void a();
//静态方法
public static void b()
{
System.out.println("静态方法。");
}
//默认方法
default static void c()
{
System.out.println("默认方法。");
}
}

使用细节

  • 接口不能被实例化;
  • 接口中所有方法都是public,接口中抽象方法可以不用使用abstract
  • 一个普通类实现接口,必须将接口中所有方法实现;
  • 抽象类实现接口时,可以不用实现接口抽象方法;
  • 一个类可以同时实现多个接口,逗号间隔两个接口(class A implements IB,IC);
  • 接口中的属性只能是final的,而且是public final static修饰的,如int a = 1;,实际上是public final static int a = 1;即必须初始化;
  • 接口中的属性访问形式:接口名.属性(静态属性访问);
  • 一个接口不能继承其他的类,但能继承其他的接口:interface A extend B,C{}
  • 接口的修饰符只能是public和默认。

接口VS.继承

  • 继承相当于父子关系,子类生来就有(自动拥有)父类的一些属性和方法;而接口相当于师徒关系,某个类若想拥有某个接口的方法,则必须去学习(实现接口中方法)。
  • 如子类需要拓展功能,可以通过实现接口的方式,实现接口可以理解为对java单继承机制的一种补充。
  • 解决问题不同
    • 继承:主要提高代码的复用性和可拓展性;
    • 接口:设计、设计好各种规范(方法),让其他人去实现这些方法;
  • 接口比继承更加灵活
    • 继承满足is-a的关系,而接口只需要满足like-a的关系;
  • 接口在一定程度上实现代码解耦(即:接口规范性+动态绑定),低耦合,高内聚。

接口多态特性

  1. 多态参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class test1 {
public static void main(String[] args) {
//接口的多态体现
//接口类型的变量 if01 可以指向实现了接口的对象实例
IF if01 = new Car();
if01 = new Bar();

//继承的多态体现
//父类类型的变量 a 可以指向继承A的子类的对象实例
A a = new B();
a = new C();
}
}

interface IF{}
class Car implements IF{}
class Bar implements IF{}

class A{}
class B extends A{}
class C extends A{}
  1. 多态数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/*
给Usb数组中,存放 Phone 和 Camera 对象,Phone 类还有一个特有的方法 call(),
遍历 Usb 数组,如果是 Phone 对象,除了调用 Usb 接口定义的方法外,还要调用 Phone 特有的方法 call()。
*/
public class test1 {
public static void main(String[] args) {
//多态数组-> 接口类型数组
Usb usb[] = new Usb[2];
usb[0] = new Phone();
usb[1] = new Camera();

for (int i = 0;i < usb.length;i++)
{
usb[i].work(); //动态绑定
//类型判断 (类型的向下转型)
if (usb[i] instanceof Phone) //判断运行类型是否为 Phone
{
((Phone) usb[i]).call(); //向下转型
}
}
}
}

interface Usb{
void work();
}
class Phone implements Usb{
public void call()
{
System.out.println("手机可以打电话。");
}
public void work()
{
System.out.println("手机工作中。");
}
}
class Camera implements Usb{
public void work()
{
System.out.println("相机工作中。");
}
}

/*
输出:
手机工作中。
手机可以打电话。
相机工作中。
*/

接口多态传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//多态传递演示
public class test1 {
public static void main(String[] args) {
IG ig = new Teacher();
/* 如果 IG 继承了 IH 接口,而 Teacher 实现了 IG 接口,
那么相当于 Teacher 实现了 IH 接口,这就是多态的传递
*/
IH ih = new Teacher();
}
}

interface IH{
void work();
}
interface IG extends IH {}
class Teacher implements IG{//接口可以继承
//当接口IH中有抽象方法时,IG也继承了该方法,故在 Teacher 类中也需要实现方法
public void work()
{
System.out.println("工作中。。。");
}
}

九、内部类

类的五大成员:属性、方法、构造器、代码块、内部类。

内部类最大特点就是可以访问私有属性。

内部类的分类

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

    • 局部内部类(有类名)
    • 匿名内部类(无类名)
  2. 定义在外部类的成员位置上

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

局部内部类

  1. 局部内部类定义在方法中代码块中,其本质仍是一个类;

  2. 可以直接访问外部类的所有成员,包括私有的;

  3. 不能添加访问修饰符,因为它的地位是一个局部变量,局部变量不能访问修饰符的,但可以使用final修饰,以保证其不被继承,因为局部变量也可以使用final;

  4. 作用域:仅在定义它的方法或代码块中;

  5. 局部内部类访问局部内部类成员:直接访问;

  6. 外部类(内部类之外的第一个类)的方法中访问局部内部类成员:创建对象,再访问;

  7. 外部其他类(外部类之外的类)不能访问局部内部类;

  8. 若外部类和局部内部类的成员重名时,默认遵循就近一致原则,如果向访问外部类的成员,使用外部类.this.成员去访问,如下代码演示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class test1 { //外部其他类
public static void main(String[] args) {
Outer outer = new Outer();
outer.m1();
System.out.println("outer对象的hashcode:"+outer);
}
}

class Outer //外部类
{
private int n1 = 100;
public void m1() //方法
{
final class Inner
{
private int n1 = 200;
public void f1()
{
System.out.println("n1="+n1); //就近一致原则,访问局部内部类的 n1=200
//此时 Outer.this 相当于一个对象(调用m1方法的对象,此时为outer)
System.out.println("n1="+Outer.this.n1); //访问外部类的 n1=100
System.out.println("Outer.this对象的hashcode:"+Outer.this);
}
}
Inner inner = new Inner();
inner.f1();
}
}
/*
输出:
n1=200
n1=100
Outer.this对象的hashcode:demo.Outer@7a5d012c
outer对象的hashcode:demo.Outer@7a5d012c
*/

匿名内部类

  1. 本质详解

    有两个需求:

    • 需求1:使用IA接口,实现接口内方法
      传统方法:单独用类实现接口方法,并创建对象,如tiger1
    • 需求2:Tiger类只使用一次,后面不再使用,单独定义一个类浪费资源
      解决方法:使用匿名内部类简化,如tiger2

    解决代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class test1 {
public static void main(String[] args) {
Outer outer = new Outer();
outer.method();
}
}

class Outer //外部类
{
private int n1 = 100;
public void method() //方法
{
//需求1
Tiger tiger1 = new Tiger();
tiger1.cry();
//需求2:使用匿名类
IA tiger2 = new IA()
{
@Override
public void cry() {
System.out.println("(匿名类)老虎叫。。。");
}
};
// IA tiger2 = () -> System.out.println("(匿名类)老虎叫。。。");
tiger2.cry();
}
}
interface IA
{
void cry();
}
//需求1
class Tiger implements IA
{
@Override
public void cry() {
System.out.println("(实现接口)老虎叫。。。");
}
}
/*
输出:
(实现接口)老虎叫。。。
(匿名类)老虎叫。。。
*/

分析:

  • tiger2的的编译类型是IA,运行内存就是匿名内部类;

  • 内部匿名类的底层:

    1
    2
    3
    4
    5
    6
    7
    class Outer$1 implements IA //Outer$1是系统分配的名称
    {
    @Override
    public void cry() {
    System.out.println("(匿名类)老虎叫。。。");
    }
    }
    • Jdk底层在创建匿名内部类 Outer$1后,立即创建了Outer$1实例,并把地址返回给tiger2;

    • 匿名内部类只使用一次就不再使用(对象可以多次使用)。

  1. 匿名内部类的使用

    • 基于类的匿名内部类

      • father编译类型是Father,运行类型是Outer$1(匿名内部类);

      • 底层会创建匿名内部类:

      • 同时也直接返回了匿名内部类Outer$1的对象。

1
2
3
4
5
6
7
class Outer$1 extend Father
{
@Override
void speak() {
System.out.println("匿名内部类重写 Father中speak方法");
}
}

如下代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
 public class test1 {
public static void main(String[] args) {
Outer outer = new Outer();
outer.method();
}
}

class Outer //外部类
{
private int n1 = 100;
public void method() //方法
{
//基于类的匿名内部类
Father father = new Father("Jack"){ //匿名内部类也可使用构造器,Jack传递给构造器
@Override
void speak() {
System.out.println("匿名内部类重写 Father中speak方法");
}
};
System.out.println("father对象的运行类型:"+father.getClass());
father.speak();
//基于抽象类的匿名内部类
Animal animal = new Animal()
{
@Override
void eat() {
System.out.println("动物吃东西。");
}
};
animal.eat();
}
}

class Father
{
String name;
public Father(String name) {
this.name = name;
System.out.println("接收到的名字:"+name);
}
void speak() {}
}
abstract class Animal
{
abstract void eat();
}
/*
输出:
接收到的名字:Jack
father对象的运行类型:class demo.Outer$1
匿名内部类重写 Father中speak方法
动物吃东西。
*/

  1. 使用细节

    • 调用可以动态绑定调用,也可以直接调用,内部类本身就会返回对象;
    • 可以访问外部类的所有成员,包括私有成员;

      • 匿名内部类访问外部类成员:直接访问;

      • 外部其他类(外部类之外的类)不能访问局部内部类;

      • 若外部类和局部内部类的成员重名时,默认遵循就近一致原则,如果向访问外部类的成员,使用外部类.this.成员去访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//动态绑定调用
Animal animal = new Animal()
{
@Override
void eat(String s) {
System.out.println("动物吃"+s);
}
};
animal.eat("草");
//直接调用
new Animal() //返回对象
{
@Override
void eat(String s) {
System.out.println("动物吃"+s);
}
}.eat("草");
  1. 应用场景

    • 当作实参直接传递,如下代码演示:

    代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/*
需求:实现一个多功能闹钟,可以提醒不同事件
解决方法:1. 接口实现:代码繁杂
2. 内部匿名类当作实参
*/

public class test1 {
public static void main(String[] args) {
Outer outer = new Outer();
outer.method();
}
}

class Outer //外部类
{
private int n1 = 100;
public void method() //方法
{
CellPhone cellPhone = new CellPhone();
//内部类直接当实参传递
cellPhone.SmartClock(new Clock() {
@Override
public void ring() {
System.out.println("起床了。");
}
});
cellPhone.SmartClock(new Clock() {
@Override
public void ring() {
System.out.println("上课了。");
}
});
}
}

interface Clock
{
void ring();
}
class CellPhone
{
public void SmartClock(Clock clock)
{
clock.ring();//动态绑定
}
}
/*
输出:
起床了。
上课了。
*/

成员内部类

  1. 定义在外部类的成员位置;

  2. 可以直接访问外部类的所有成员,包含私有的;

  3. 可以添加任意访问修饰符,因为它的地位就是类的一个成员;

  4. 作用域:和外部类的其他成员一样,作用域为整个外部类体;

  5. 成员内部类访问外部类:直接访问;

  6. 外部类访问内部类:创建对象再访问;

  7. 若外部类和局部内部类的成员重名时,默认遵循就近一致原则,如果向访问外部类的成员,使用外部类.this.成员去访问;

  8. 外部其他类访问成员内部类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*
需求:外部其他类访问成员内部类成员
*/
public class test1 {
public static void main(String[] args) {
//方法1:相当于把new Inner()当作是outer的成员
Outer outer = new Outer();
Outer.Inner inner1 = outer.new Inner();
inner1.say();
//方法2:在外部类中,编写一个方法,可以返回Inner对象
Outer.Inner inner2 = outer.getInner();
inner2.say();
}
}

class Outer //外部类
{
private int n1 = 100;
public class Inner {
void say()
{
System.out.println("成员内部类输出。");
}
}
//方法2中获取对象的方法
public Inner getInner() //返回Inner类型的方法
{
return new Inner();
}
}
/*
输出:
成员内部类输出。
成员内部类输出。
*/

静态内部类

  1. 定义在外部类的成员位置,并且有static修饰;

  2. 可以直接访问外部类的所有静态成员,包含私有的,但不能直接访问非静态成员;

  3. 可以添加任意访问修饰符,因为它的地位就是类的一个成员;

  4. 作用域:和外部类的其他成员一样,作用域为整个外部类体;

  5. 静态内部类访问外部类:直接访问所有静态成员;

  6. 外部类访问静态内部类:创建对象再访问;

  7. 若外部类和局部内部类的成员重名时,默认遵循就近一致原则,如果向访问外部类的成员,使用外部类.成员去访问;

  8. 外部其他类访问静态内部类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/*
需求:外部其他类访问静态内部类成员
*/
public class test1 {
public static void main(String[] args) {
//方法1:静态成员可以直接通过 类名.成员 访问(满足访问权限时)
Outer outer = new Outer();
Outer.Inner inner1 = new Outer.Inner();
inner1.say();
//方法2:在外部类中,编写一个方法,可以返回Inner对象实例
Outer.Inner inner2 = outer.getInner();
inner2.say();
//当getInner()为静态时,也可应直接用类名访问,如下
//Outer.getInner().say();
}
}

class Outer //外部类
{
private int n1 = 100;
static class Inner {
void say()
{
System.out.println("静态内部类输出。");
}
}
//方法2中获取对象的方法
public static Inner getInner() //返回Inner类型的方法
{
return new Inner();
}
}
/*
输出:
静态内部类输出。
静态内部类输出。
*/