自用的学习笔记,若有错误欢迎指出。

回顾方法

方法的定义

  1. 修饰符

  2. break和return的区别

    • break:跳出switch循环,结速循环
    • return:代表方法的结束,返回一个结果
  3. 方法名

    • 注意命名规范,要做到见名知意
  4. 参数列表

    • 参数类型
    • 参数名

方法的调用

  1. 静态方法 static

    • static是和类一起加载的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//静态方法 static
public class Demo02{
public static void main(String[] args){
//实例化这个类 new
//对象类型 对象名 = 变量值
Student student = new Student();
student.say();

}
}


//这里在单独新创建一个类
public class Student{
//定义一个方法
public void say(){
System.out.println("hello");
}
}
  1. 非静态方法
1
2
3
4
5
6
7
8
9
10
public class Demo02{
public static void main(String[] args){
//实际参数和形式参数的类型要对应
Demo02 demo02 = new Demo02();
demo02.add(1, 3);
}
public int add(int a, int b){
return a + b;
}
}

java值传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Demo04{
public static void main(String[] args){
int a = 1;
System.out.println(a);

Demo04.change(a);
System.out.println(a);//输出值为:1
}

//返回值为空
public static void change(int a){
a = 10;
}
}

Java引用传递

引用传递的是对象,其本质还是值传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Demo01 {
public static void main(String[] args) {
Student student = new Student();
System.out.println(student.name); //null

Demo01.change(student);
System.out.println(student.name); //Ben
}

public static void change(Student student){
/*student是一个对象
是指向的Student student = new Student();
这个是一个具体的人,可以改变属性。
*/
student.name = "Ben";
}
}

//定义了一个Student类,有一个属性:name
class Student{
String name; //null
}

类与对象的关系

  • 类似一种抽象的数据类型,它是对某一类事物整体描述/定义,但是并不能代表某一个具体的事物。
    • Person类、Pet类、Car类等,这些类都是用来描述/定义某一类具体的事物应该具备的特点和行为。
  • 对象是抽象概念的具体实例
    • 能够体现出特点,展现出功能的是具体的实例,而不是一个抽象的概念。
    • 张三就是一个人的具体实例,张三家里的旺财就是狗的一个具体实例。

创建与初始化对象

  • 使用new关键字来创建对象
  • 使用new关键字创建的时候,除了分配内存空间之外,还会给创建好的对象进行默认的初始化以及对类中的构造器的调用。
  • 类中的构造器也称为构造方法,是在进行创建对象的时候必须要调用的。并且构造器有以下两个特点:
    • 必须和类的名字相同
    • 必须没有返回类型,也不能写void
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
//一个项目应该只存在一个main方法
public class Application {
public static void main(String[] args) {
//类是抽象的,需要实例化
//类实例化后会返回一个自己的对象
Student josh = new Student();
Student tom = new Student();

//赋值
josh.name = "josh";
josh.age = 23;

System.out.println(josh.name); //josh
System.out.println(josh.age); //23
}
}

public class Student {
//属性:字段
String name; //null
int age; //0

//方法
public void study(){
System.out.println(this.name + "在学习中");
}
}

构造器

  • 使用new关键字,必须要有构造器。其本质是在调用构造器。
  • 有参构造:一旦定义了有参构造,无参就必须显示定义
  • 构造器的定义:
    • 和类名字相同
    • 没有返回值
  • 构造器的作用:
    • 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
public class Application {
public static void main(String[] args) {

//new 实例化一个对象
Person person = new Person();

System.out.println(person.name); //josh
}
}

public class Person {

String name;

//这是一个无参构造器
public Person(){

}

//实例化初始值
public Person(){
this.name = "josh";
}

//idea:alt + insert可以自动生成构造器
}

面向过程 & 面向对象

  • 面向过程思想
    • 步骤清晰简单
    • 面对过程适合处理一些较为简单的问题
  • 面向对象思想
    • 物以类聚,分类的思维模式,思考问题首先会解决问题需要哪些分类,然后对这些分类进行单独思考。最后,才对某个分类下的细节进行面向过程的思索。
    • 面向对象适合处理复杂的问题,适合处理需要多人协作的问题。
  • 对于描述复杂的事物,为了从宏观上把握、从整体上合理分析,我们需要使用面向对象的思路来分析整个系统。但是,具体到微观操作,仍然需要面向过程的思路去处理。

面向对象

  • 面向对象编程(Object-Oriented Programming,OOP)
  • 面向对象编程的本质就是:以类的方式组装代码,以对象的组织(封装)数据
  • 抽象
  • 三大特性
    • 封装
    • 继承
    • 多态

从认识论角度考虑是先有对象后有类。对象,是具体的事物。类,是抽象的,是对对象的抽象。

从代码运行角度考虑是先有类后有对象。类是对象的模板。

类与对象的定义以及关系

:类是对现实生活中一类具有共同属性和行为的事物的抽象

对象:如果将类理解为一张设计图, 根据设计图,可以创建出具体存在的事物

类和对象关系:总结来说, 类是对象的模板,可以根据类来创建对象。

成员变量局部变量的区别

区别:

  1. 类中编写位置不同
    成员变量: 方法外
    局部变量: 方法里
  2. 初始化值不同
    成员变量: 有默认初始化值
    局部变量: 没有默认初始化值, 使用之前必须完成赋值
  3. 内存位置不同
    成员变量: 堆内存
    局部变量: 栈内存
  4. 生命周期不同
    成员变量: 随着对象的创建而存在, 随着对象的消失而消失
    局部变量: 随着方法的调用而存在, 随着方法的弹栈而消失

封装

封装的定义:

  • 就是用类设计对象处理某一个事物的数据时,应该把要处理的数据,以及处理这些数据的方法,设计到一个对象中去。
  • 面向对象的三大特征:封装继承多态

封装的设计规范:

  • 合理隐藏,合理暴露
  • 在设计对象的时候,对象的成员变量我们应该隐藏起来,并且暴露合适的get & set方法来供外界访问。
  • 对象的成员方法,只暴露业务必须用到的方法即可。

代码层面控制对象的成员公开或者隐藏:

  • 公开成员,可以使用public进行修饰。
  • 隐藏成员,可以使用private进行修饰。

我们程序设计要追求:“高内聚,低耦合”。高内聚就是类的内部数据操作细节自己完成,不允许外部干涉;低耦合就是仅暴露少量的方法给外部使用。

  • 封装(数据的隐藏)
    • 通常,应该禁止直接访问一个对象中数据的实际表示,而应该通过操作接口来访问,这称为信息隐藏。
    • 属性私有: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
public class Application{
public static void main(String[] args){
Student s1 = new Student();

//使用set方法
s1.setName("josh");
//使用get方法
System.out.println(s1.getName());

s1.setAge(23);
System.out.println(s1.getAge());
}
}

//idea中,使用快捷键:alt+insert快速生成get/set方法
public class Student{
//属性私有
private String name;
private int age;
private char sex;

//提供一些可以操作这些属性的方法
//提供可以允许被其他类调用的get和set方法

//get获得这个数据
public String getName(){
return this.name;
}

//set给这个数据设置值
public void setName(String name){
this.name = name;
}

public int getAge(){
return this.age;
}
public void setAge(int age){
if(age > 120 || age < 0){
System.out.println("你输入的年龄有误");
} else {
this.age = age;
}
}
}
  • 封装的意义:
    • 提高程序的安全性,保护数据
    • 隐藏代码的实现细节
    • 统一接口
    • 提高了系统的可维护性

继承

继承的本质是对某一批类的抽象,从而实现对现实世界更好的建模。

  • extends的意思是“扩展”,子类是父类的扩展。

  • java中类只有单继承,没有多继承

  • 继承是类和类之间的一种关系。除此之外,类和类之间的关系还有依赖、组合、聚合等。

  • 继承关系的两个类,一个为子类(派生类),一个为父类(基类)。子类继承父类,使用关键字extends来表示。

  • 子类和父类之间,子类继承父类:subClass extends superClass

继承的特点:

  • 子类可以继承父类种所有非私有的成员(成员变量、成员方法)
  • 继承后子类的对象是子类和父类共同完成的
  • Java是单继承的,Java中的类不支持多继承,但是支持多层继承

涉及:

  • object类
  • super
  • 方法重写

子类继承了父类,就会有父类的全部方法。

在java中,所有的类都默认直接或间接继承Object类

注意点:

  • super调用父类的构造方法,必须在构造方法的第一个
  • super必须只能出现在子类的方法或者构造方法中
  • superthis不能同时调用构造方法

superthis的区别:

  • 代表的对象不同
    • this为本身调用者这个对象
    • super代表父类对象的引用
  • 前提:
    • this没有继承也可以使用
    • super只能在继承条件下使用
  • 构造方法:
    • this();在本类里面的构造
    • 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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class Application{
public static void main(String[] args){
Student student = new Student();
student.speak(); //say hello
System.out.println(student.money); //10000

/*
*分别输出:
* Ben
* fm
* josh
*/
student.test("Ben");

/*
*分别输出:
* Student
* Student
* Person
*/
student.test1();
}
}

//Person类为父类
//在java中,所有的类都默认直接或间接继承Object类
public class Person{
public int money = 10000;
protected String name = "josh";

public void speak(){
System.out.println("say hello");
}

public void print(){
System.out.println("Person");
}
}

//Student作为子类来继承父类Person
//子类继承了父类,就会有父类的全部方法
public class Student extends Person{
private String name = "fm";

public void test(String name){
System.out.println(name); //访问参数name
System.out.println(this.name); //访问Student类里面的name
System.out.println(super.name); //访问父类Person类里面的name
}
public void print(){
System.out.println("Student");
}

public void test1(String name){
print();
this.print();
super.print();
}
}

权限修饰符

修饰符的作用范围:private < 缺省 < protected < public

修饰符 在本类中 同一个包下的其他类里 任意包下的子类里 任意包下的任意类里
private T
缺省 T T
protected T T T
public T T T T

方法的重写

概述:当子类觉得父类中的某个方法不好用,或者无法满足自己的需求时,子类可以重写一个方法名称、参数列表一样的方法,去覆盖父类的这个方法,这就是方法重写。

注意事项:

  • 重写小技巧:使用Override注解,该注解可以指定Java编译器,检查我们方法重写的是否正确,代码可读性也会更好

  • 子类重写父类方法时,访问权限必须大于或者等于父类该方法的权限。

    • 例如:如果父类的是public,我们把它变成private就不行
    • public > protected > 缺省(default) > private
  • 重写方法返回值类型,必须与被重写方法的返回值类型一样,或者范围更小

  • 私有方法、静态方法不能被重写,如果重写会报错

  • 重写都是方法的重写,和属性无关

  • 静态方法和非静态方法的区别很大!

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
public class Application{
public static void main(String[] args){

A a = new A();
a.test(); //A->test()

//方法的调用只和左边,定义的数据类型有关
//父类的引用指向类子类
B b = new A();
b.test(); //B->test()
}
}

public class B{
public static void test(){
System.out.println("B->test()");
}
}

public class A extends B{

public static void test(){
System.out.println("A->test()");
}
}

当我们使用@Override来重写,可以看到子类重写了父类的方法。

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
public class Application{
public static void main(String[] args){

A a = new A();
a.test(); //A->test()

//子类重写了父类的方法
B b = new A();
b.test(); //B->test()
}
}

public class B{
public static void test(){
System.out.println("B->test()");
}
}

public class A extends B{

@Override //重写,附带有功能的注释
public void test(){
System.out.println("A->test()");
}

}

重写需要有继承关系,子类重写的是父类的方法。

  1. 方法名必须相同
  2. 参数列表必须相同
  3. 修饰符:范围可以扩大,不能缩小。(如果父类的是public,我们把它变成private就不行 (public > protected > default > private))
  4. 抛出异常:范围可以被缩小,但不能扩大。
  5. 子类的方法必须和父类的一致,但是方法体不同。

为什么需要重写:

  1. 父类的功能,子类不一定需要,或者不一定满足。

多态

多态即同一个方法可以根据可以根据发送对象的不同而采用多种不同的行为方式。

一个对象的实际类型是确定的,但可以指向对象的引用类型有很多

  • 多态存在的条件
    • 继承关系
    • 子类重写父类方法
    • 父类引用指向子类对象
  • 多态注意事项:
    • 多态是方法的多态,属性没有多态
    • 父类和子类必须要有联系,不是同一类型进行转换就会报错(类型转换异常:ClassCastException
    • 无法实现多态:
      • static方法,这个是属于类,它不属于实例
      • final常量
      • private方法
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
public class Application{
public static void main(String[] args){
//一个对象的实际类型是确定的
//可以指向的引用类型是不确定的
//Student能调用的方法,都是自己的,或者是父类的方法
Student s1 = new Student();
Student s2 = new Student();

//父类的引用指向子类
//父类型不能调用子类独有的方法
Person s3 = new Student();
Object s4 = new Student();

/*
* 对象能执行哪些方法,主要看对象在左边的类型
* 和右边关系不大。
*/
s3.run(); //子类重写了父类的方法,执行子类的方法,输出结果为:play
s1.run(); //play
}
}

public class Person{
public void run(){
System.out.println("run");
}
}

public class Student extends Person{
@Override
public void run(){
System.out.println("play");
}

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

多态的好处

在多态形式下,右边对象是解耦合的,更便于扩展和维护。

1
2
People p1 = new Student();
p1.run();

定义方法时候,使用父类类型的形参,可以接收一切子类对象,扩展性更强、更便利。

例如:可以直接传参设为父类People类,以便于接受所有子类的传入。

1
2
3
public void go(People p) {
p.run();
}

类型转换

多态会产生的问题:多态下不能使用子类独有的功能。需要用到强制类型转换来实现。

自动类型转换

多态向上转型格式:

父类 变量名 = new 子类()

People p = new Teacher();

强制类型转换

多态向下转型格式:
子类类型 变量名 = (子类类型) 父类变量名;

Teacher t = (Teacher) P;

多态形式下不能直接调用子类特有方法,但是转型后是可以调用的。这里所说的转型就是把父类变量转换为子类类型。格式如下:

1
2
3
4
5
//如果p接收的是子类对象
if(父类变量 instance 子类){
//则可以将p转换为子类类型
子类 变量名 = (子类)父类变量;
}

强制类型转换的注意事项:

  1. 存在继承/实现关系就可以在编译阶段进行强制类型转换,编译阶段不会报错
  2. 运行时,如果发现对象的真实类型强转后的类型不同,就会报类型转换异常 ClassCastException的错误出来。

关于多态转型问题,我们最终记住一句话:原本是什么类型,才能还原成什么类型

在强制类型转换之前,我们可以通过instanceof关键字来判断该对象指向的真实类型,再进行强制转换。

instanceof 关键词

instanceof关键字中用来判断类和类之间是否拥有父子关系。

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 Application {
public static void main(String[] args) {

//Object > String
//Object > Person > Teacher
//Object > Person > Student
Object object = new Student();

System.out.println(object instanceof Person); //true
System.out.println(object instanceof Student); //true
System.out.println(object instanceof Object); //true
System.out.println(object instanceof Teacher); //false
System.out.println(object instanceof String); //false

//======================================

Person person = new Student();
System.out.println(person instanceof Object); //true
System.out.println(person instanceof Student); //true
System.out.println(person instanceof Person); //true
System.out.println(person instanceof Teacher); //false
}
}

public class Person {

}

public class Student extends Person{

}

public class Teacher extends Person{

}

注意点:

  1. 父类引用指向子类的对象。
  2. 子类转换为父类,向上转型,可以直接转换
  3. 父类转换为子类,向下转型,需要进行强制转换
  4. 进行转换是为了方便方法的调用,减少重复的代码,保证了代码的简洁。
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 Application {
public static void main(String[] args) {
//类型之间的转换:父类 子类

//高 低
Person student = new Student();
// student.go(); //会编译报错

//将这个对象转换为student类型,我们才可以使用student类型的方法了
//高等级转换为低等级的需要强制转换

Student student1 = (Student)student;
student1.play(); //强制转换后可以使用

//=================================================

Student student = new Student();
student.play();

//子类转换为父类,可能会丢失自己本来的一些方法
Person person = student;
// person.play(); //编译失败

}
}

public class Person {
public void run(){
System.out.println("run");
}
}

public class Student extends Person{
public void play(){
System.out.println("play");
}
}

static关键字详解

  • 叫静态,可以修饰成员变量、成员方法。

成员变量按照有无static修饰,分为两种:

  • 类变量: 有static修饰,属于类,在计算机里只有一份,会被类的全部对象共享。
  • 实例变量(对象的变量): 无static修饰,属于每个对象。

关于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
//static
public class Student {
private static int age;
private double score;

public static void main(String[] args) {
Student s1 = new Student();

System.out.println(Student.age); //这个是类变量
System.out.println(s1.score);
System.out.println(s1.age);

Student.go(); //静态方法可以直接调用
s1.run(); //要调用非静态方法需要new
}

public void run(){
run(); //非静态方法可以直接调用静态方法
}

public static void go(){

}
}

其实一个类里面还存在有一个匿名代码块。同时,需要注意的是在类里面每一个代码块的加载时间是不同的。从上到下分别是:

  1. static代码块是和类一起加载的,同时也是加载速度最快的。并且该代码块也只能执行一次。
  2. 匿名代码块是可以用来赋予初始值的,为第二个被加载。
  3. 构造方法,最后被加载。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Person {

public static void main(String[] args) {
Person p1 = new Person();
Person p2 = new Person();


}

//2:赋予初始值
{
System.out.println("匿名代码块");
}

//1:和类一起加载的,第一个被加载。并且也只执行一次
static {
System.out.println("静态代码块");
}

//3
public Person(){
System.out.println("构造方法");
}
}

static的应用知识

代码块

概述:代码块是类的5大成分之一(成员变量、构造器、方法、代码块、内部类)

代码块分为两种:

  • 静态代码块:
    • 格式: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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class Student{
static int number = 18;
static String schoolName;
//静态代码块
static {
//只会执行一次
System.out.println("静态代码块执行了一次");
//初始化类变量(使用不多,了解即可)
schoolName = "华一";
}

//实例代码块
{
System.out.println("实例代码块被执行了");
//重复代码可以放在实例代码块中
System.out.println("有人创建了对象");
}

public Student() {
System.out.println("无参数构造器被执行了");
}

public Student(String name) {
System.out.println("有参数构造器被执行了" + name);
}

}


public class Test {
public static void main(String[] args) {
//先输出执行:静态代码块执行了一次
//再输出number:18
System.out.println(Student.number);
//输出:18
System.out.println(Student.number);

//先输出:实例代码块被执行了
//有人创建了对象
//再输出:无参数构造器被执行了
Student s1 = new Student();

//先输出:实例代码块被执行了
//有人创建了对象
//再输出:有参数构造器被执行了Josh
Student s2 = new Student("Josh");
}
}

单例设计模式

什么是设计模式:

  • 一个问题通常有N种方法来解答,其中肯定有一种解法是最优的,这个最优的解法被人总结出来了,称之为设计模式
  • 设计模式有20多种,对应20多种软件开发种会遇到的问题
  • 是具体问题的最优解决方案

主要关注该设计模式:1. 可以解决什么问题; 2. 设计模式的写法

单例设计模式:

  • 确保一个类只有一个对象

示例:

  • 饿汉式单例
  • 懒汉式单例

饿汉式单例

拿对象时,对象就创建好了

  • 私有类的构造器
  • 定义一个类变量记住类的一个对象
  • 定义一个类方法,返回对象

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class A {
//2. 定义一个类变量记住类的一个对象
private static A a = new A();
//1. 必须私有类的构造器
private A() {

}
//3. 定义一个类方法,返回对象
public static A getObject() {
return a;
}
}

单例设计模式的应用场景

查看Java的Runtime类源码。Java在运行时候,只有一套运行环境,所以该Runtime类只需要一个对象。

电脑的任务管理器也是单例模式,由于任务管理器只需要一个窗口来显示,创建多个也没有意义还占用电脑的内存影响性能。所以也做成单例模式来优化避免浪费内存。

懒汉式单例

拿对象时,对象才开始创建

写法:

  • 私有类的构造器
  • 定义一个类变量记住类的一个对象
  • 定义一个类方法,保证返回的是同一个对象

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class B {
//2. 定义一个类变量记住类的一个对象
//和饿汉式单例不同的是,先不在此处创建对象
private static B b;
//1. 必须私有类的构造器
private B() {

}
//3. 定义一个类方法,该方法保证第一次调用时才会创建一个对象
//后面调用时都会使用同一个对象返回
public static A getObject() {
//当b == null时代表还没有创建b对象,是第一次创建对象
if (b == null) {
b = new B();
}
return b;
}
}

总结

当该对象被频繁的使用时候,使用饿汉式单例。如果一个对象使用次数用的不多则可以使用懒汉式单例。

this关键字

this代表当前类对象的引用 (地址) 哪一个对象调用的方法, 方法中的this, 就代表的是哪一个对象

a1.show() —> this —> a1的地址
a2.show() —> this —> a2的地址

this关键字使用场景:

当成员变量和局部变量出现了重名问题, Java会使用就近原则, 优先使用局部变量,这时候要想做区分的话, 可以使用this关键字.

final类

  1. final关键字是最终的意思,可以修饰(类、方法、变量)
  2. 修饰类:该类被称为最终类,特点是不能被继承了
  3. 修饰方法:该方法被称为最终方法,特点是不能被重写了。
  4. 修饰变量:该变量只能被赋值一次

抽象类

  • abstract修饰符可以用来修饰方法也可以修饰类,如果修饰方法,那么该方法就是抽象方法;如果修饰类,那么该类就是抽象类。
  • 抽象类可以没有抽象方法,但是抽象方法的类一定要申明为抽象类。
  • 抽象类不能使用new关键字来创建对象,它是用来让子类继承的。
  • 抽象方法,只有方法的申明,没有方法的实现,它是用来让子类去实现的。
  • 子类继承抽象类,那么就必须要去实现抽象类中没有实现的抽象方法,否则该子类也要去申明为抽象类。
  • 抽象类最主要的特点:抽象类不能创建对象,仅作为一种特殊的父类,让子类继承并实现。
  • 一个类继承抽象类,必须重写完抽象类的全部抽象方法,否则这个类也必须定义成抽象类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//abstract抽象类
//抽象类中可以写普通方法
//抽象方法必须在抽象类中
//抽象的抽象:约束
public abstract class Action {
//抽象方法:只有方法名字,没有方法的实现
//抽象类不能被new出来,只能靠子类去实现它
public abstract void doSomeThing();

}

//抽象类的所有方法,继承了它的子类,都必须要实现它的方法
//除非子类也是抽象类
public class A extends Action{
//如果有一个类要继承抽象类,就必须要重写doSomeThing()方法
@Override
public void doSomeThing(){

}
}

思考?:

  • 抽象类不能new对象出来,所以抽象类不存在构造器。
  • 抽象类的存在是为了方便,提高了开发的效率。

抽象类的运用

总结:

  1. 用抽象类可以把父类中相同的代码,包括方法声明都抽取到父类,这样能更好的支持多态,一提高代码的灵活性。

  2. 反过来用,我们不知道系统未来具体的业务实现时,我们可以先定义抽象类,将来让子类去实现,以方便系统的扩展。

Animal类,但是由于猫和狗叫的声音不一样,于是我们在Animal类中将叫的行为写成抽象的。代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class Animal {
private String name;

//动物叫的行为:不具体,是抽象的
public abstract void cry();

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

接着写一个Animal的子类,Dog类。代码如下

1
2
3
4
5
public class Dog extends Animal{
public void cry(){
System.out.println(getName() + "汪汪汪的叫~~");
}
}

然后,再写一个Animal的子类,Cat类。代码如下

1
2
3
4
5
public class Cat extends Animal{
public void cry(){
System.out.println(getName() + "喵喵喵的叫~~");
}
}

最后,再写一个测试类,Test类。

1
2
3
4
5
6
7
public class Test2 {
public static void main(String[] args) {
// 目标:掌握抽象类的使用场景和好处.
Animal a = new Dog();
a.cry(); //这时执行的是Dog类的cry方法
}
}

假设现在系统有需要加一个Pig类,也有叫的行为,这时候也很容易原有功能扩展。只需要让Pig类继承Animal,复写cry方法就行。

1
2
3
4
5
6
public class Pig extends Animal{
@Override
public void cry() {
System.out.println(getName() + "嚯嚯嚯~~~");
}
}

此时,创建对象时,让Animal接收Pig,就可以执行Pig的cry方法

1
2
3
4
5
6
7
public class Test2 {
public static void main(String[] args) {
// 目标:掌握抽象类的使用场景和好处.
Animal a = new Pig();
a.cry(); //这时执行的是Pig类的cry方法
}
}

模板方法设计模式

模板方法模式主要解决方法中存在重复代码的问题

示例:

如我们所见,在A类和B类中存在很多相同的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class A {
public void sing() {
System.out.println("选一首你喜欢的歌");
System.out.println("我是一只小小小小鸟...");
System.out.println("播放完毕");
}
}

public class B {
public void sing() {
System.out.println("选一首你喜欢的歌");
System.out.println("倘若那天...");
System.out.println("播放完毕");
}
}

我们定义一个父类,同时将相同的代码都保留下来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//模板方法设计模式
public abstract class C {
//该方法会变成模板方法,同时建议使用final修饰该方法
//模板方法是不能够被子类重写的
public final void sing() {
System.out.println("选一首你喜欢的歌");
//去掉不同的:System.out.println("倘若那天...")
doSing();
System.out.println("播放完毕");
}

//定义抽象方法
public abstract void doSing();
}

之后我们需要将A类B类继承C类,同时重写方法

1
2
3
4
5
6
7
8
9
10
11
12
public class A extends C {
@Override
public void doSing() {
System.out.println("我是一只小小小小鸟...");
}
}
public class B extends C {
@Override
public void doSing() {
System.out.println("倘若那天...");
}
}

扩展:java中DateFormat类使用的是模板方法设计模式

接口

接口就是规范,定义的是一组规则。体现了现实世界中:“如果你是…就必须去…”的思想。比如:如果你是猎人,就必须去打猎。

Java提供了一个关键字interface,用这个关键字来定义接口这种特殊结构。格式如下

1
2
3
4
public interface 接口名{
//成员变量(常量)
//成员方法(抽象方法)
}

接口是用来被类实现(implements)的,我们称之为实现类。

1
2
3
4
//格式
修饰符 class 实现类 implements 接口1, 接口2, 接口3, ... {

}

一个类是可以实现多个接口的(接口可以理解成干爹),类实现接口必须重写所有接口的全部抽象方法,否则这个类也必须是抽象类

声明类的关键字是class,声明接口的关键字是interface

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
//接口都需要用实现类
//implements 关键字
public class UserServiceImpl implements UserService, TimeService{

//实现了接口的类就需要重写接口中的方法。
@Override
public void add(String addName) {

}

@Override
public void delete(String deleteName) {

}

@Override
public void update(String updateName) {

}

@Override
public void search(String searchName) {

}

@Override
public void timer() {

}
}

//接口都需要用实现类
public interface UserService {

//在接口里面定义的属性都是常量
//这里public static final可以加,可以不加。
//public static final int AGE = 22;
int AGE = 22;

//接口中的所有定义的方法都是抽象的 abstract
void add(String addName);
void delete(String deleteName);
void update(String updateName);
void search(String searchName);

}

public interface TimeService {
void timer();
}

接口的作用:

  1. 约束。
  2. 定义一些方法,让不同的人来使用。
  3. 在接口里面所定义的方法都是public abstract
  4. 在接口里面定义的变量都是常量:public static final
  5. 接口不能被直接实例化,接口中没有构造方法。
  6. 关键字implements可以实现多个接口。
  7. 必须要重写接口中的方法@Override

接口的好处

  • 弥补了类单继承的不足,一个类同时可以实现多个接口。
  • 让程序可以面向接口编程,这样程序员可以灵活方便的切换各种业务实现。
  • 一个类我们说可以实现多个接口,同样,一个接口也可以被多个类实现的。这样做的好处是我们的程序就可以面向接口编程了,这样我们程序员就可以很方便的灵活切换各种业务实现了。

现在要写一个A类,想让他既是老师,偶然也是司机能够开车,偶尔也是歌手能够唱歌。那我们代码就可以这样设计,如下:

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
class Teacher{

}

interface Driver{
void drive();
}

interface Singer{
void sing();
}

//A类是Teacher的子类,同时也实现了Dirver接口和Singer接口
class A extends Teacher implements Driver, Singer{
@Override
public void drive() {

}

@Override
public void sing() {

}
}

public class Test {
public static void main(String[] args) {
//想唱歌的时候,A类对象就表现为Singer类型
Singer s = new A();
s.sing();

//想开车的时候,A类对象就表现为Driver类型
Driver d = new A();
d.drive();
}
}

综上所述:接口弥补了单继承的不足,同时可以轻松实现在多种业务场景之间的切换。

接口JDK8的新特性

随着JDK版本的升级,在JDK8版本以后接口中能够定义的成员也做了一些更新,从JDK8开始,接口中新增的三种方法形式。

我们看一下这三种方法分别有什么特点?

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
public interface A {
/**
* 1、默认方法:必须使用default修饰,默认会被public修饰
* 实例方法:对象的方法,必须使用实现类的对象来访问。
*/
default void test1(){
System.out.println("===默认方法==");
//在默认方法中调用静态方法
test2();
}

/**
* 2、私有方法:必须使用private修饰。(JDK 9开始才支持的)
* 实例方法:对象的方法。
*/
private void test2(){
System.out.println("===私有方法==");
}

/**
* 3、静态方法:必须使用static修饰,默认会被public修饰
*/
static void test3(){
System.out.println("==静态方法==");
}

void test4();
void test5();
//如需新增功能,则可以定义default方法
default void test6(){

}
}

定义一个B类来实现A接口,需要重写A类中的所有抽象方法。但是,其中默认方法default不需要重写。

1
2
3
4
5
6
7
8
9
10
11
public class B implements A{
@Override
public void test4() {

}

@Override
public void test5() {

}
}

最后在主程序中,三种方法的调用如下:

1
2
3
4
5
6
7
8
public class Test {
public static void main(String[] args) {
B b = new B();
b.test1(); //默认方法使用对象调用
// b.test2(); //A接口中的私有方法,B类调用不了,可以将该方法的调用写在A接口的默认方法中
A.test3(); //静态方法,使用接口名调用
}
}

综上所述:JDK8对接口新增的特性,有利于对程序进行扩展。

接口的其他细节

一个接口可以继承多个接口

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
interface A{
void test1();
}
interface B{
void test2();
}
interface C{}

//比如:D接口继承C、B、A
interface D extends C, B, A{

}

//E类在实现D接口时,必须重写D接口、以及其父类中的所有抽象方法。
class E implements D{
@Override
public void test1() {

}

@Override
public void test2() {

}
}

综上所述:一个接口可以继承多个接口,接口同时也可以被类实现。

接口除了上面的多继承特点之外,在多实现、继承和实现并存时,有可能出现方法名冲突的问题,需要了解怎么解决(仅仅作为了解即可,实际上工作中几乎不会出现这种情况)

  1. 一个接口继承多个接口,如果多个接口中存在相同的方法声明,则此时不支持多继承
  2. 一个类实现多个接口,如果多个接口中存在相同的方法声明,则此时不支持多实现
  3. 一个类继承了父类,又同时实现了接口,父类中和接口中有同名的默认方法,实现类会有且只使用父类的方法
  4. 一个类实现类多个接口,多个接口中有同名的默认方法,则这个类必须重写该方法

内部类

  • 内部类就是在一个类的内部在定义一个类,比如在A类中定义一个B类,那么B类就相对与A类来说就是内部类,而A类相对于B类来说就是外部类了。
  • 成员内部类
  • 静态内部类
  • 局部内部类
  • 匿名内部类[重要]

内部类是类中的五大成分之一(成员变量、方法、构造器、内部类、代码块),如果一个类定义在另一个类的内部,这个类就是内部类。

当一个类的内部,包含一个完整的事物,且这个事物没有必要单独设计时,就可以把这个事物设计成内部类。

比如:汽车、的内部有发动机,发动机是包含在汽车内部的一个完整事物,可以把发动机设计成内部类。

成员内部类

成员内部类就是类中的一个普通成员,类似于成员变量、成员方法。

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
public class Outer {
private int id = 207;
public int age;

public void out(){
System.out.println("这是内部类的方法");
}

public class Inner{
private int id = 310;
public void in(){
System.out.println("这是内部类");
}

public void getId(){
//在内部类中既可以访问自己类的成员,也可以访问外部类的成员
System.out.println("获得外部类的私有属性");
int id = 202;
System.out.println(id); //202
System.out.println(this.id); //310
System.out.println(Outer.this.id); //207
}
}

}

public class Application {
public static void main(String[] args) {
Outer outer =new Outer();
//通过这个外部类来实例化内部类
Outer.Inner inner = outer.new Inner();
inner.in();
inner.getId();
}
}

总结一下内部类访问成员的特点

  • 既可以访问内部类成员、也可以访问外部类成员
  • 如果内部类成员和外部类成员同名,可以使用**类名.this.成员**区分

静态内部类

静态内部类,其实就是在成员内部类的前面加了一个static关键字。静态内部类属于外部类自己持有。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Outer {
private int age = 99;
public static String schoolName="黑马";

// 静态内部类
public static class Inner{
//静态内部类访问外部类的静态变量,是可以的;
//静态内部类访问外部类的实例变量,是不行的
public void test(){
System.out.println(schoolName); //99
//System.out.println(age); //报错
}
}
}

静态内部类创建对象时,需要使用外部类的类名调用。

1
2
3
//格式:外部类.内部类 变量名 = new 外部类.内部类();
Outer.Inner in = new Outer.Inner();
in.test();

局部内部类

局部内部类是定义在方法中的类,和局部变量一样,只能在方法中有效。所以局部内部类的局限性很强,一般在开发中是不会使用的。

类可以写在多个地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Outer {
public void method(){

//局部内部类
class Inner{
public static void main(String[] args) {
System.out.println("在方法中也可以加入class");
}
}
}
}

//一个java类里面只能有一个public类,但是可以有多个class类
class Test{

}

匿名内部类

匿名内部类是一种特殊的局部内部类;所谓匿名,指的是程序员不需要为这个类声明名字。

下面就是匿名内部类的格式:

1
2
3
4
new 父类/接口(参数值){
@Override
重写父类/接口的方法;
}

匿名内部类本质上是一个没有名字的子类对象、或者接口的实现类对象。

这个是匿名内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Outer {
public static void main(String[] args) {
//没有名字初始化类,不用将实例保存在变量中
new Test().run();

new UserService(){
@Override
public void say(){
System.out.println("say hello");
}
};
}
}

//一个java类里面只能有一个public类,但是可以有多个class类
class Test{
public void run(){
System.out.println("run");
}
}

interface UserService{
void say();
}

匿名内部类的作用:简化了创建子类对象、实现类对象的书写格式。

匿名内部类的应用场景

只有在调用方法时,当方法的形参是一个接口或者抽象类,为了简化代码书写,而直接传递匿名内部类对象给方法。这样就可以少写一个类。比如,看下面代码

1
2
3
public interface Swimming {
public void swim();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Test {
public static void main(String[] args){
Swimming s1 = new Swimming(){
public void swim(){
System.out.println("狗游泳飞快");
}
};
go(s1);

Swimming s1 = new Swimming(){
public void swim(){
System.out.println("猴子游泳不太行");
}
};
go(s1);
}
//形参是Swimming接口,实参可以接收任意Swimming接口的实现类对象
public static void go(Swimming s){
System.out.println("开始~~~~~~~~");
s.swim();
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 Test {
public static void main(String[] args){
//直接把匿名内部类作为参数传入
go(new Swimming(){
public void swim(){
System.out.println("狗游泳飞快");
}
});

go(new Swimming(){
public void swim(){
System.out.println("猫猫游不了泳");
}
});
}
//形参是Swimming接口,实参可以接收任意Swimming接口的实现类对象
public static void go(Swimming s){
System.out.println("开始~~~~~~~~");
s.swim();
System.out.println("结束~~~~~~~~");
}
}

泛型

所谓泛型指的是,在定义类、接口、方法时,同时声明了一个或者多个类型变量(如:<E>),称为泛型类、泛型接口、泛型方法、它们统称为泛型。

比如我们前面学过的ArrayList类就是一个泛型类,我们可以打开API文档看一下ArrayList类的声明。

ArrayList集合的设计者在定义ArrayList集合时,就已经明确ArrayList集合时给别人装数据用的,但是别人用ArrayList集合时候,装什么类型的数据他不知道,所以就用一个<E>表示元素的数据类型。

当别人使用ArrayList集合创建对象时,new ArrayList<String> 就表示元素为String类型,new ArrayList<Integer>表示元素为Integer类型。

我们总结一下泛型的作用、本质:

  • 泛型的好处:在编译阶段可以避免出现一些非法的数据。

  • 泛型的本质:把具体的数据类型传递给类型变量。

自定义泛型类

接下来我们学习一下自定义泛型类,但是有一些话需要给大家提前交代一下:泛型类,在实际工作中一般都是源代码中写好,我们直接用的,就是ArrayList这样的,自己定义泛型类是非常少的。

自定义泛型类的格式如下

1
2
3
4
//这里的<T,W>其实指的就是类型变量,可以是一个,也可以是多个。
public class 类名 <T, W> {

}

接下来,我们自己定义一个MyArrayList泛型类,模拟一下自定义泛型类的使用。注意这里重点仅仅只是模拟泛型类的使用,所以方法中的一些逻辑是次要的,也不会写得太严谨。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//定义一个泛型类,用来表示一个容器
//容器中存储的数据,它的类型用<E>先代替用着,等调用者来确认<E>的具体类型。
public class MyArrayList<E>{
private Object[] array = new Object[10];
//定一个索引,方便对数组进行操作
private int index;

//添加元素
public void add(E e){
array[index]=e;
index++;
}

//获取元素
public E get(int index){
return (E)array[index];
}
}

接下来,我们写一个测试类,来测试自定义的泛型类MyArrayList是否能够正常使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test{
public static void main(String[] args){
//1.确定MyArrayList集合中,元素类型为String类型
MyArrayList<String> list = new MyArrayList<>();
//此时添加元素时,只能添加String类型
list.add("张三");
list.add("李四");

//2.确定MyArrayList集合中,元素类型为Integer类型
MyArrayList<Integer> list1 = new MyArrayList<>();
//此时添加元素时,只能添加String类型
list.add(100);
list.add(200);

}
}

自定义泛型接口

泛型接口其实指的是在接口中把不确定的数据类型用<类型变量>表示。定义格式如下:

1
2
3
4
//这里的类型变量,一般是一个字母,比如<E>
public interface 接口名<类型变量>{

}

比如,我们现在要做一个系统要处理学生和老师的数据,需要提供2个功能,保存对象数据、根据名称查询数据,要求:这两个功能处理的数据既能是老师对象,也能是学生对象。

首先我们得有一个学生类和老师类

1
2
3
4
5
6
public class Teacher{

}
public class Student{

}

我们定义一个Data<T>泛型接口,T表示接口中要处理数据的类型。

1
2
3
4
5
public interface Data<T>{
public void add(T t);

public ArrayList<T> getByName(String name);
}

接下来,我们写一个处理Teacher对象的接口实现类

1
2
3
4
5
6
7
8
9
10
11
//此时确定Data<E>中的E为Teacher类型,
//接口中add和getByName方法上的T也都会变成Teacher类型
public class TeacherData implements Data<Teacher>{
public void add(Teacher t){

}

public ArrayList<Teacher> getByName(String name){

}
}

接下来,我们写一个处理Student对象的接口实现类

1
2
3
4
5
6
7
8
9
10
11
//此时确定Data<E>中的E为Student类型,
//接口中add和getByName方法上的T也都会变成Student类型
public class StudentData implements Data<Student>{
public void add(Student t){

}

public ArrayList<Student> getByName(String name){

}
}

在实际工作中,一般也都是框架底层源代码把泛型接口写好,我们实现泛型接口就可以了。

泛型方法

下面就是泛型方法的格式

1
2
3
public <泛型变量,泛型变量> 返回值类型 方法名(形参列表){

}

下面在返回值类型和修饰符之间有定义的才是泛型方法。

1
2
3
4
5
6
7
8
//是泛型方法
public static <T> void test(T t){

}
//不是
public E get(int index) {
return (E) arr[index];
}

泛型限定

通配符

就是?,可以在使用泛型 的时候代表一切类型;ETKV是在定义泛型的时候使用。

泛型的上下限:

  • 泛型上限:? extends Car:? 能接收的必须是Car或者其子类
  • 泛型下限:? super Car :? 能接收的必须是Car或者其父类
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
public class Test{
public static void main(String[] args){
//调用test方法,传递字符串数据,那么test方法的泛型就是String类型
String rs = test("test");

//调用test方法,传递Dog对象,那么test方法的泛型就是Dog类型
Dog d = test(new Dog());
}

//这是一个泛型方法<T>表示一个不确定的数据类型,由调用者确定
public static <T> test(T t){
return t;
}

ArrayList<People> peoples = new ArrayList<>();
peoples.add(new People());
peoples.add(new People());

ArrayList<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());

ArrayList<Student> students = new ArrayList<>();
students.add(new Student());

test2(peoples);
test2(students);

//所有的传入的参数类型都可以匹配
test3(peoples);
test3(dogs);

//可以定义带有继承类型的
public static <T extends People> void test2(ArrayList<T> t) {

}

//"?"代表通配符,使用范型的时候可以代表 一切类型
public static void test3(ArrayList<?> t) {

}

//"? extends People"上限继承:传入的参数只能是People和People的子类
public static void test3(ArrayList<? extends People> t) {

}
}
public class Dog {

}


public class People {

}

public class Student extends People {

}

泛型注意事项

泛型擦除,也就是说泛型只能编译阶段有效,一旦编译成字节码,字节码中是不包含泛型的。而且泛型只支持引用数据类型,不支持基本数据类型。

泛型不支持基本数据类型,只能支持引用数据类型。

后记

之后就是java中最重要的面向对象编程。加油!去吧,继续服务。 ( ゚ ▽ ゚)つ□乾杯~

参考链接

B站:狂神说java老师讲的真的非常好,有兴趣的话就去看看吧。

封面图来源:https://www.pixiv.net/artworks/87550307