✨你好啊,我是“ 罗师傅”,是一名程序猿哦。
🌍主页链接:楚门的世界 - 一个热爱学习和运动的程序猿
☀️博文主更方向为:分享自己的快乐 briup-jp3-ing
❤️一个“不想让我曾没有做好的也成为你的遗憾”的博主。
💪很高兴与你相遇,一起加油!

前言

目标:Java基础编程,熟练Java开发语法和规则,养成良好编程习惯

abstract

抽象概述

抽象,简单可理解为不具体、高度概括的,专业描述为:抽象是一种将复杂的概念和现实世界问题简化为更易于理解和处理的表示方法。在计算机科学和编程中,抽象是一种关注问题的本质和关键特征,而忽略具体实现细节的方法。

抽象是通过定义类、接口和方法来实现的

抽象方法:将共性的行为(方法)抽取到父类之后,发现该方法的实现逻辑无法再父类中给出具体的实现,就可以将该方法定义尾抽象方法。(父类无法具体实现就抽象

抽象类:如果一个类中存在抽象方法,那么该类就必须声明为抽象类

抽象特点

1
2
3
4
5
6
7
8
9
//抽象方法定义格式
public abstract 返回值类型 方法名(参数列表);
//特别注意:抽象方法只有方法声明,没有方法的实现
//抽象类定义格式
[权限修饰符] abstract class 类名 {
//0或多个数据成员
//0或多个构造方法
//0或多个成员方法
}

抽象类和抽象方法的关系:(重点掌握)

  • 使用abstract修饰的类就是抽象类
  • 抽象类可以包含,也可以不包含抽象方法
  • 包含抽象方法的类,一定要声明为抽象类

抽象类和普通类区别:

  • 抽象类必须使用abstract修饰符
  • 抽象类相对普通类,多了包含抽象方法的能力
  • 抽象类相对普通类,失去了实例化创建对象的能力(抽象类不能实例化!)

抽象类和普通类相同点:

  • 符合继承关系特点,能够使用多态机制
  • 子类可以重写从抽象类继承的方法
  • 实例化子类对象需要借助父类构造器实现父类部分的初始化

案例:

要求:定义一个形状类Shape,包含抽象方法getArea(),再定义其子类Circle,重写抽象方法并进行功能测试。

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
abstract class Shape {
private String name;

public Shape(String name) {
this.name = name;
}

public void show() {
System.out.println(this.name + "面积 = " + getRoundedValue(getArea(), 4));
}

/**
* 获取指定位小数,四舍五入后的结果
*
* @param number 原double值
* @param digit 希望得到小数点后几位
* @return 四舍五入后的结果
*/
private BigDecimal getRoundedValue(double number, int digit) {
BigDecimal decimal = BigDecimal.valueOf(number);
BigDecimal roundedValue = decimal.setScale(digit, BigDecimal.ROUND_HALF_UP);
return roundedValue;
}

// 不确定具体是什么图形,无法计算该面积 故定义为抽象方法
public abstract double getArea();
public abstract double getPerimeter();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Circle extends Shape {
private double radius;

public Circle(String name, double radius) {
super(name);
this.radius = radius;
}

@Override
public double getArea() {
return 2 * Math.PI * radius * radius;
}

@Override
public double getPerimeter() {
return 2 * Math.PI * radius;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
abstract class Rectangle extends Shape { // 子类也设置为抽象类
private double wight;
private double length;

public Rectangle(String name, double wight, double length) {
super(name);
this.wight = wight;
this.length = length;
}

@Override
public double getArea() {
return wight * length;
}

// 因为子类有抽象方法没有实现,所以需要设置成抽象类
// @Override
// public double getPerimeter() {
// // TODO Auto-generated method stub
// return 0;
// }
}
1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
//1.抽象类不能实例化对象,下面一行编译报错
//Shape p = new Shape(); //error
//2.抽象类引用可以指向子类(非抽象类)对象
Shape p = new Circle("圆",2);
//3.实际开发中,主要多态应用
double area = p.getArea();
System.out.println("area: " + area);
}

interface

引用数据类型:类、数组、接口

接口概述

接口是对Java单继承的补充。Java只支持单继承(亲爹唯一),如果在开发过程中想额外增强类的功能,可以借助接口实现(可以拜师,拜多个师傅也可 以)。

接口是Java中一种重要的抽象机制,它提供了一种定义行为规范和实现多态性的方式。通过合理使用接口,可以提高代码的可扩展性、可维护性和灵活性

接口是除了类和数组之外,另外一种引用数据类型,需要使用interface关键字来定义,接口最终也会被编译成.class文件,但一定要明确接口并不是类,而是另一种引用数据类型

接口基础定义格式:

1
2
3
4
5
6
[修饰符] interface 接口名{
// 数据成员,可以定义多个
[public static final] 数据类型 数据成员 = 值;
// 抽象方法:可以定义多个
[public abstract] 返回值类型 方法名(形参列表);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//使用interface关键字来定义接口
interface IAction {
//数据成员,下面2行效果一样
//public static final int NUM = 10;
int NUM = 10;
//成员方法,下面2行效果一样
//public abstract void start();
void start();
public abstract void end();
}

public static void main(String[] args) {
//接口不可以实例化对象
//IAction ac = new IAction(); error
System.out.println(IAction.NUM);
//接口中数据成员默认 public static final,故而下行编译报错
//IAction.NUM = 20;
}

补充内容:

  • JDK8中,还允许在接口中编写静态方法和默认方法

  • JDK9中,还允许在接口中编写私有方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface IAction {
// 抽象方法
public abstract void start();

// 静态方法
static void staticMethod() {
System.out.println("static method");
}

// 默认方法
default void defaultMethod() {
System.out.println("default method");
}

// 私有方法
private void privateMethod() { // JDK9后才不会报错
System.out.println("private method");
}
}

接口实现

  • Java中类和类之间的关系是继承,且只能是单继承

  • 类和接口是实现关系,通过implements关键字表示,可以是单实现,也可以是多实现

  • 子类还可以在继承一个父类的同时实现多个接口

接口的实现类书写格式:

1
2
3
4
//一个类可以同时实现多个接口
[修饰符] class 类名 implements 接口名1,接口名2,... {
重写所有抽象方法
}

注意事项:

  • 接口属于引用数据类型的一种,它不是类,它没有构造方法
  • 接口的实现类(子类),可以是正常类(重写所有抽象方法),可以是抽象类(包含抽象方法)
  • 接口不能创建对象,一般用接口引用指向实现类对象
1
2
3
4
5
6
7
8
9
10
11
//定义接口的实现类, 重写所有的抽象方法
class ActionImpl implements IAction {
@Override
public void start() {
System.out.println("start开始执行 ");
}
@Override
public void end() {
System.out.println("执行完成,end结束");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
//1.接口不能实例化对象,下面一行编译报错
//IAction ic = new IAction();
//2.接口引用 指向实现类对象
IAction ac = new ActionImpl();
//3.接口数据成员访问测试
System.out.println(IAction.NUM);
System.out.println(ac.NUM);
System.out.println("-------------");
//4.通过接口引用 调用 重写方法(多态体现)
ac.start();
ac.end();
}

注意事项:

  • 在类和接口的实现关系中,可以使用多态,因为类和接口的实现关系,可理解为继承的一种形式。
  • 一个类可以同时实现多个接口,但需要把多个接口的抽象方法全部重写

接口继承

Java中,类和类之间是单继承关系,接口和接口之间是多继承

接口继承格式:

1
2
3
[修饰符] interface 子接口 extends 父接口1,父接口2...{
// 新增成员或抽象方法
}
1
2
3
4
5
6
7
8
9
10
interface Runable {
void run();
}
interface Flyable {
void fly();
}
//接口多继承
interface Action extends Runable,Flyable {
void sing();
}

综合案例

案例描述: 定义一个抽象父类Animal,再定义两个接口IJumpAble跳火圈、ICycleAble骑自行车,最后定义一个猴子类Monkey,去继承Animal,同时实现IJumpAble、 ICycleAble,进行功能测试。

复杂实现类定义格式:

1
2
3
[修饰符] class 实现类 extends 父类 implements 接口名1, 接口名2,... {
重写所有方法;
}
  • 抽象父类 + 接口
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
interface IJumpAble {
public abstract void jump();
}

interface ICircleAble {
public abstract void circle();
}

abstract class Animal {
private String name;
private int age;
private String color;

public Animal(String name, int age, String color) {
super();
this.name = name;
this.age = age;
this.color = color;
}

@Override
public String toString() {
return "Animal [name=" + name + ", age=" + age + ", color=" + color + "]";
}

public abstract void eat();

public abstract void sleep();
// 省略get | set ...
}
  • Monkey
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
class Monkey extends Animal implements ICircleAble, IJumpAble {
public Monkey(String name, int age, String color) {
super(name, age, color);
}

@Override
public void jump() {
System.out.println(getName() + " jump fire circle");
}

@Override
public void circle() {
System.out.println(getName() + " driver car");
}

@Override
public void eat() {
System.out.println(getName() + " eat bananar");
}

@Override
public void sleep() {
System.out.println(getName() + " hanging up to sleep ");
}
}
  • TestCode
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 static void main(String[] args) {
//1.抽象类不能实例化对象,但可以定义引用指向子类对象
Animal2 a = new Monkey("yellow", 2, "小悟空");
//2.借助抽象类引用,只能访问抽象类中具有的方法
a.eat();
a.sleep();
//下面两行编译报错【多态:编译看左边,运行看右边】
//a.jump();
//a.cycle();
System.out.println("****************");
//3.用接口引用指向实现类对象
ICycleAble c = new Monkey("yellow", 5, "马戏团小猴");
//4.借助接口引用调用接口中重写方法
c.cycle();

//注意:接口引用类型 只能调用 接口中具备的方法【多态:编译看左边,运行看右边】
//下面三行编译报错
//c.eat(); error
//c.sleep(); error
//c.jump(); error
System.out.println("****************");

//5.如果想要调用父类方法或其他接口方法,可以借助类型转换实现
//注意,一定要借助instanceof额外判断引用指向对象的类型
if (c instanceof Monkey) {
Monkey m = (Monkey) c;
m.eat();
m.sleep();
}
System.out.println("****************");
if (c instanceof IJumpAble) {
IJumpAble j = (IJumpAble) c;
j.jump();
}
}

注意事项:

  • 接口多态应用时,编译看左边,运行看右边
  • 即接口引用只能调用接口中包含的方法,成功调用的是重写以后的方法

类接口关系

  • 类与类的关系
    • 继承关系,只能单继承,但是可以多层继承
  • 类与接口的关系
    • 实现关系,可以单实现,也可以多实现,还可以在继承一个类的同时实现多个接口
  • 接口与接口的关系
    • 继承关系,可以单继承,也可以多继承

接口新特性

  • 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
interface JDK8Action {
String oPS_MODEString = "auto";

void start();

void stop();

// 下面是JDK8新特性
// 默认方法
public default void dFun() {
System.out.println("in default fun() ...");
}

// 静态方法
public static void sFun() {
System.out.println("in static fun() ...");
}
}

class Demo01 implements JDK8Action {

@Override
public void start() {
System.out.println("重写start() ...");
}

@Override
public void stop() {
System.out.println("重写stop() ...");
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
// 1.接口引用指向实现类对象
JDK8Action a = new Demo01();

// 2.调用实现类重写方法
a.start();
a.stop();

// 3.调用default方法
a.dFun();

//4.JDK8中接口可以定义static方法,但不能通过接口引用调用,只能通过接口名调用
//a.sFun(); // This static method of interface JDK8Action can only be accessed as JDK8Action.sFun
JDK8Action.sFun();
}

注意事项:JDK8接口可以定义static方法,但不能通过接口引用调用,只能通过接口名调用

  • JDK9新特性:接口可以包含私有方法
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
//使用interface关键字来定义接口
interface JDK9Action {
// 接口中的静态常量
String OPS_MODE = "auto";
// 接口中的抽象方法
void start();
//私有方法 jdk9以下报错
private void run() {
System.out.println("private run() ...");
}
}
class Demo02 implements JDK9Action {
@Override
public void start() {
System.out.println("重写start() ...");
}
}
//测试类
public class Test026_JDK9 {
public static void main(String[] args) {
//1.接口引用指向实现类对象
JDK9Action a = new Demo02();
//2.调用实现类重写的抽象方法
a.start();
//3.调用接口private方法
//a.run();无法调用接口中私有的方法,私有方法只是供内部使用
}
}

常见面试题

接口和抽象类有什么区别?如何选择?

语法结果区别:

  • 定义方式:抽象类通过使用abstract关键字来定义,而接口使用interface关键字来定义
  • 实现方式:一个类可以继承(extends)一个抽象类,而一个类可以实现(implements)多个接口
  • 构造函数:抽象类可以有构造函数,而接口不能有构造函数
  • 方法实现:抽象类可以包含具体的方法实现,而接口只能包含抽象方法,即没有方法体的方法声明
  • 多继承:Java不支持多继承,一个类只能继承一个抽象类,但可以实现多个接口
  • 数据成员:抽象类可以包含普通数据成员和static数据成员,接口只能包含static final修饰的数据成员

设计理念区别:

  • 不同的实现类之间体现 like a 的关系,接口更加强调行为规范的定义,适用于多个类具有相同行为规范的情况。例如:飞机具备飞翔的行为,鸟也具备飞翔的行为,此时我们就可以定义接 口包含抽象方法fly(),然后让飞机和鸟分别去实现该接口。飞机 like a 鸟, 因为它们都会fly()。
  • 子类和抽象父类体现的是 is a 的关系,抽象类归根到底还是类,它比较特殊,不能被实例化,只能被继承。抽象类用于定义一种通用的模板或者规范,其中可包含一些具体数据成员、方法实现和抽象方法声明。例如:前面案例中的形状类Shape,它里面包含方法getArea(),但该方法功 能不确定,所以定义成抽象方法,而包含了抽象方法的类Shape也必须被声 明为抽象类。定义子类圆形类,其getArea()方法功能是明确的,则子类中重 写方法。

结论:

  • 如果仅仅是要额外扩展已存在类的功能,则选择定义接口,让类去实现接口
  • 如果需要创建一组相关的类,且这些类之间有一些共同的行为和属性,那么可以定义一个类作为这些类的父类。如果不想实例化父类对象,则可以把这个父类设置为抽象类。

内部类

在一个类的内部再定义另外的一个类,这就是内部类。

正常类形式,可以称为外部类或顶级类或平行类

1
2
public class Test{}
class Level{}

内部类形式:

1
2
3
4
5
6
//外部类
public class Outer {
//内部类
public class Inner {
}
}

成员内部类

在类中,除了可以定义成员方法、成员变量、还可以定义成员内部类

成员内部类定义格式:

1
2
3
4
5
6
7
8
9
10
11
//外部类
[修饰符] class 外部类 {
//省略...
//内部类
[权限修饰符] class 内部类 {
0或多个数据成员
0或多个构造方法
0或多个成员方法
//注意,不可以包含static成员或方法
}
}

内部类对象实例化格式:

1
2
外部类名.内部类名 对象名 = 外部类对象.内部类对象;
例:Outer.Inner oi = new Outer().new Inner();

内部类中访问外部类中同名成员

1
2
外部类名.this.成员名;
例: int v = Outer.this.o_num;

注意事项:

  • 成员内部类中可以直接访问外部类中所有成员和方法(含private、static字段)
  • 在外部类中可以直接访问内部类所有的成员和方法(含private),但必须借助内部类对象
  • 成员内部类内部不能定义static成员或方法(静态字段是与类本身相关联的,而内部类的实例是依赖于外部类实例的)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class OuterClass {
private static int staticField = 10;
private int instanceField = 20;

class InnerClass {
// 这里不能定义静态字段
// static int innerStaticField = 30;

void accessFields() {
System.out.println(staticField); // 可以访问外部类的静态字段
System.out.println(instanceField); // 可以访问外部类的实例字段
}
}
}
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
package com.briup.chap07.blog;

import com.briup.chap07.blog.Outer.Inner;

public class Test031_Member {

public static void main(String[] args) {
Outer outer = new Outer();
outer.disp();
System.out.println("-----------------------------");
Inner inner = outer.new Inner();
inner.show();
System.out.println("-----------------------------");
Inner inner2 = new Outer().new Inner();
inner2.show();

}
}

class Outer {
private int o_num = 10;
private int num = 20;

class Inner {
private int i_num = 30;
private int num = 40; // 同名

// public static int s_num = 50; error
// public static void s_test() {}

void show() {
System.out.println("in Inner, o_num: " + o_num);
int num = 50;
System.out.println("in Inner, num: " + num); // 局部
System.out.println("in Inner, num: " + this.num); // 内部类成员变量
System.out.println("in Inner, num: " + Outer.this.num);
}
}

// 外部类方法访问内部字段
public void disp() {
Outer.Inner inn = this.new Inner();
System.out.println("in Outer, i_num: " + inn.i_num);

Inner inn2 = new Inner();
System.out.println("inn2.i_num: " + inn2.i_num);
}
}

注意,上述代码编译成功后,外部类和内部类都会生成对应的class文件

JavaAPI中的使用成员内部类: java.util.ArrayList 类中,就定义了好几个成员内部类,并且还是private 修饰的

思考,什么情况下会使用内部类?

在对事物进行抽象的时候,若一个事物内部还包含其他事物,就可以考虑使用内部类这种结构。

例如,汽车(Car)中包含发动机(Engine) ,这时, Engine 类就可以考虑(非 必须)使用内部类来描述,定义在Car类中的成员位置。 这样设计,既可以表示Car和Engine的紧密联系的程度,也可以在Engine类中很 方便的使用到Car里面的属性和方法

注意:这里从程序中,类与类之间的关系和意义进行考虑而设计的,其实这里即使不使用内部类的结构,使用普通的两个类也能完成功能,但是内部类的结构设计会更加符合实际意义,也能够好的完成功能,因为内部类访问外部的属性和方法会更加容易。

静态内部类

静态内部类属于成员内部类中的一种,其需要static关键字进行修饰。

1
2
3
4
5
6
7
8
9
10
11
//外部类
[修饰符] class 外部类 {
//省略...
//静态内部类
[权限修饰符] static class 内部类 {
0或多个数据成员
0或多个构造方法
0或多个成员方法
//注意,不可以包含static成员或方法
}
}

对象定义格式:

1
2
外部类名.内部类名 对象名 = new 外部类名.内部类名();
Outer2.Inner inn = new Outer2.Inner();

静态方法访问格式:

1
2
外部类名.内部类名.方法名();
// 如果在外部类中,外部类名可以省略。

注意事项:

  • 相对于成员内部类,静态内部类中可以定义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
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
package com.briup.chap07.blog;

public class Test032_Static {

public static void main(String[] args) {
// 1.静态内部类调用static方法
// 编译报错,不识别Inner,会把Inner当成外部类去查找
// Inner.sFun();
// 正确写发
Outer2.Inner.sFun();
System.out.println("---------------");
// 2.静态内部类普通方法调用
// 内部类对象 固定定义格式
Outer2.Inner inn = new Outer2.Inner();
inn.show();
System.out.println("----------------");
// 3.外部类对象访问内部类成员
Outer2 outer = new Outer2();
outer.disp();

}
}

class Outer2 {
private int o_num = 10;
private static int s_num = 20;

static class Inner {
private int i_num = 30;

public static int is_num = 40;

// static成员方法只能访问外部类static成员【静态只能访问静态】
public static void sFun() {
// 编译报错
// System.out.println("Outer2 o_num: " + o_num);
// cannot make a static reference to the non-static field o_num
System.out.println("Outer2 s_num: " + Outer2.s_num);
System.out.println("Outer2 s_num: " + s_num);
}

// 普通成员方法
void show() {
// 1.访问内部类中数据成员
System.out.println("in Inner, num: " + this.i_num); // 30
System.out.println("in Inner, is_num: " + Inner.is_num); // 40
// 2.静态内部类方法中只能访问外部类的static成员
// 编译报错
// System.out.println("in Inner,o_num: " + o_num);
// 3.访问外部类static成员,下面两行效果相同
System.out.println("in Inner, s_num: " + s_num);
// 20
System.out.println("in Inner, s_num: " + Outer2.s_num); // 20
}

}

// 外部类方法访问内部字段
public void disp() {
// Outer2.Inner inn = new Inner(); 一样的
Outer2.Inner inn = new Outer2.Inner();
System.out.println("in Outer, i_num: " + inn.i_num);
System.out.println("in Outer, is_num: " + Inner.is_num);
System.out.println("in Outer, is_num: " + inn.is_num);
}
}

注意,在静态内部类中访问不了外部类中的非静态属性和方法

JavaAPI中静态内部类案例: java.lang.Integer 类中,就定义了一个静态内部类,并且还是private修饰 的

局部内部类(了解即可)

在外部类的方法中定义的内部类,我们称为局部内部类,它的作用范围只是在当前方法中

局部内部类是最不常用的一种内部类,大家了解即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[修饰符] class 外部类 {
//省略...
//成员方法
[修饰符] 返回值类型 方法名(形式参数列表) {
//功能实现省略...
//局部内部类定义
class 成员内部类名 {
0或多个数据成员
0或多个构造方法
0或多个成员方法
}
//注意:局部内部类的作用范围在当前方法中,只能在该方法中使用
}
}

注意事项:局部内部类只能在定义的方法中使用。

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
package com.briup.chap07.blog;

public class Test033_Local {

public static void main(String[] args) {
Outer3 outer3 = new Outer3();
outer3.innerFun();
}
}

class Outer3 {
private int num = 10;

// 包含局部内部类的方法
public void innerFun() {
// 下面两行效果一样
// int num = 20; 会默认帮你加上final
final int num = 20;
// 在方法内部,定义局部内部类
class Inner {
private int i_num = 30;

public void test() {
System.out.println("局部变量num: " + num);
System.out.println("内部类成员变量this.i_num: " + this.i_num);
System.out.println("外部类成员变量Outer3.this.num: " + Outer3.this.num);
// 方法中声明的局部变量,只要在内部类中使用,默认会加上final修饰
// 所以下面一行 编译会报错:给final变量赋值
// num = 60;
}
}
// 创建局部内部类对象
Inner inn = new Inner();
// 访问内部类私有成员
System.out.println(inn.i_num);
System.out.println("-------------");
// 调用内部内方法
inn.test();
}
}

面试常考:JDK8中,方法中定义的局部变量,如果在局部内部类中对其访问操作,那么这个局部变量会默认加上final修饰,其值不能改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class LocalInnerClassExample {
public void outerMethod() {
int outerVariable = 10;

// 在局部内部类中访问外部方法的局部变量
class Inner {
void innerMethod() {
System.out.println("Outer variable: " + outerVariable);
// outerVariable = 20; // 这里如果解除注释会导致编译错误
}
}

Inner inner = new Inner();
inner.innerMethod();
}

public static void main(String[] args) {
LocalInnerClassExample example = new LocalInnerClassExample();
example.outerMethod();
}
}

匿名内部类(必须掌握)

匿名内部类,是一种没有名字的内部类,本质上是一个特殊的局部内部类(定义在方法内部)。

在之后的代码中,匿名内部类是使用最多的内部类(必须掌握)。

常规接口、抽象类的使用匿名内部类步骤:

  • 声明一个类,去实现这个接口,或去继承抽象类
  • 重写所有抽象方法
  • 用接口或引用去指向子类或实现类对象
  • 通过接口或抽象类引用去调用重写的方法

在这个过程中,我们的核心任务是重写抽象方法,最后再调用这些重写的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
父类或接口类型 变量名 = new 父类或接口(构造方法实参列表) {
// 重写所有的抽象方法
@Override
public 返回值类型 method1(形参列表) {
方法体实现
}
@Override
public 返回值类型 method2(形参列表) {
方法体实现
}
//省略...
};
//匿名内部类对象调用方法
变量名.重写方法(实参列表);

匿名内部类的两种形式:

  • 利用父类,进行声明并创建匿名内部类对象,这个匿名内部类默认就是这个父类的子类型
  • 利用接口,进行声明并创建匿名内部类对象,这个匿名内部类默认就是这个接口的实现类

匿名内部类注意事项:

  • 匿名内部类必须依托于一个接口或一个父类(可以是抽象类,也可以是普通类)
  • 匿名内部类在声明的同时,就必须创建出对象
  • 匿名内部类中无法定义构造器

名内部类实现接口案例:

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
package com.briup.chap07.blog;

public class Test034_Interface {
public static void main(String[] args) {
ISleep s1 = new ISleep() {

@Override
public void sleep() {
System.out.println("躺着睡");
}
};
s1.sleep();

new ISleep() {

@Override
public void sleep() {
System.out.println("水里睡");
}
}.sleep();
}
}

interface ISleep {
void sleep();
}

匿名内部类实现抽象类案例:

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
//定义抽象类
abstract class MyThread {
//抽象方法
public abstract void run();
//普通方法
public void test() {
System.out.println("in Mythread,test ...");
}
}
public class Test034_Abstract {
public static void main(String[] args) {
//1.普通写法
MyThread th = new MyThread() {
@Override
public void run() {
System.out.println("重写 run1");
}
};
th.run();
th.test();
System.out.println("--------------");
//2.简化写法
new MyThread() {
public void run() {
System.out.println("in run2 ...");
}
}.run();
//注意,匿名对象只能使用一次,因为没有名字,无法再次访问
}
}

匿名内部类对象使用父类构造方法案例

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
package com.briup.chap07.blog;

public class Test034_Constructor {
public static void main(String[] args) {
Animal3 a1 = new Animal3() {

@Override
public void sleep() {
// 注意:匿名内部类属于子类,在重写方法中可通过super关键字访问父类方法或成员
System.out.println(super.getName() + " 喜欢吃鱼");
}

@Override
public void eat() {
System.out.println(super.getName() + " 睡觉");
}
};
// 父类引用调用重写方法
a1.sleep();
a1.eat();
System.out.println("-------------------------");

// 实例化子类对象,调用父类构造器对父类进行初始化
new Animal3("tom") {

@Override
public void sleep() {
System.out.println(super.getName() + " 喜欢吃鱼");
}

@Override
public void eat() {
System.out.println(super.getName() + " 睡觉");
}
}.eat();

}
}

abstract class Animal3 {
private String name;

public Animal3() {
}

public Animal3(String name) {
this.name = name;
}

public String getName() {
return name;
}

// 抽象方法
public abstract void eat();

public abstract void sleep();
}

内部类应用场景选择:

  • 考虑这个内部类,如果需要反复的进行多次使用(必须有名字)
    • 需要定义静态的属性和方法,选择使用静态内部类
    • 需要方位外部类的非静态属性和方法,选择使用成员内部类
  • 考虑这个内部类,如果只需要使用一次(可以没有名字)
    • 选择匿名内部类
    • 局部内部类,几乎不会用

包装类

Java中有八种基本数据类型,它们只能表示一些最简单的数字,这些数字最 小的在内存中占1个字节8位,最大占8个字节64位。这些都是简单的数字, 不是对象,所以不能用来调用方法或者属性,功能不够强大。

概述

针对这八种基本类型,JavaAPI又专门提供了对应的类类型,目的就是为了分别把这八种基本类型的数据,包装成对应的类类型,这时候就变成对象了,就可以 调用方法了或者访问属性了。

具体使用查看jdk.8文档

注意:一定要掌握 public static int parseInt(String s) 方法

装箱拆箱

JDK1.5或以上,可以支持基本类型和包装类型之间的自动装箱、自动拆箱。这简化了基本类型和包装类型之间的转换。

  • 自动装箱:基本数据类型值 自动转化为 包装类对象
  • 自动拆箱:包装类对象 自动转化为 基本数据类型值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.briup.chap07.blog;

public class Test043_Integer {
public static void main(String[] args) {
// JDK1.5之前
Integer i1 = new Integer(1);
Integer i2 = Integer.valueOf(1);

// JDK1.5之后
Integer i3 = 1; // 自动装箱,这里会自动把数字1包装成Integer类型的对象

// JDK1.5之前
Integer i4 = new Integer(1);
int i5 = i4.intValue();

// JDK1.5之后
Integer o = new Integer(1);
int i7 = o; // 自动拆箱,这里会自动把对象o拆箱为一个int类型的数字,并把数字赋值给int类型的变量i7;
}
}

其他的基本类型和包装类型之间的转换,与此类似

注意事项:

  • 使用基本类型和包装类时,要考虑隐式类型转换
  • 不同类型的基本数据类型和包装类,是不可以自动装箱拆箱的,例如int和Long
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void test1(int i) {
}
public void test2(Integer i) {
}
public void test3(long i) {
}
public void test4(Long i) {
}
public static void main(String[] args) {
Test043_Test t=new Test043_Test();
t.test1(1);// 编译通过 int i = 1; 正常赋值
t.test2(1);// 编译通过 Integer i = 1; 自动装箱
t.test3(1);// 编译通过 long i = 1; 隐式类型转换
// 编译报错
// 错误的代码:Long i = 1;
// int和Long 之间没有任何关系
//t.test4(1);
t.test4(1L);// 编译通过 Long i = 1L; 自动装箱
}

Integer缓冲区

在Java中方法区有一个叫做运行时常量池(Runtime Constant Pool)的区域,用于存储编译期间生成的各种字面量和符号引用,Integer缓冲区==Integer常量池就存在运行时常量池里面。

Integer常量池是一个特殊的缓存机制,用于存储在范围**[-128, 127]之间的Integer常量对象。这个范围是Java中一个固定(JDK默认)范围**,超出这个范围的整形常量不会被缓存。(可以通过配置JVM参数进行修改默认值)

当使用自动装箱(Autoboxing)将一个整数赋值为一个Integer对象时,如果该整数在**[-128, 127]范围内,那么会从Integer常量池中获取一个已经存在的对象,而不是创建一个新的对象。这是因为在这个范围内的整数常见且频繁使用,通过复用对象可以节省内存空间和提高性能。**

基础知识补充:

java.lang.System类中一个方法:

该方法会返回对象的哈希码,即Java 根据对象在内存中的地址计算出来一个整数值,不同的地址算出来的结果不一样的

注意,哈希码并不是对象的内存地址

1
2
3
4
5
6
Integer i5 = 128; // 超出的[-128, 127]的范围相当于new了一个新的对象
Integer i6 = 128;
System.out.println("i5: " +System.identityHashCode(i5));
System.out.println("i6: " +System.identityHashCode(i6));
System.out.println("i5 == i6: "+ (i5 == i6));
//false

Integer对象内存构成:

Object类常量池

Object类是所有类的父类型,类中定义的方法,java中所有对象都可以调用

toString

我们可以根据自己的需要重写这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Student {
private String name;
private int age;

@Override
public String toString() { // 这个是ecplise生成的
return "Student [name=" + name + ", age=" + age + "]";
}

@Override
public String toString() { // 这个是idea生成的
return "Student{" + "name='" + name + '\'' + ", age=" + age + '}';
}
}

//注意,输出对象时,自动调用对象.toString()方法
Student s1 = new Student();
//下面两行效果相同
System.out.println(s1);
System.out.println(s1.toString());

getClass

它返回引用变量运行时所指向的字节码对象

该方法是native修饰的本地方法,不是Java语言实现的

注意:子类中不能重写getClass,调用的一定是Object中的getClass方法。故获取的是运行时引用变量所指向的字节码对象

1
2
3
4
5
6
7
8
9
package com.briup.chap07.test;
public class Test052_GetClass {
public static void main(String[] args) {
Test052_GetClass tg = new Test052_GetClass();
System.out.println(tg.getClass());
}
}
//运行结果 class + 全类名
class com.briup.chap07.test.Test052_GetClass

getClass针对多态时可以获取具体指向的类对象,判断是否符合要求

equals

该方法可以比较俩个对象是否相等

注意:一般情况下,程序员不需要手动重写equals方法,STS中提供了自动 生成(建议)的快捷键:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 默认没有使用jdk1.7的模板生成的equals
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Stu other = (Stu) obj;
if (age != other.age)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}

hashCode

该方法返回一个int值,该int值是JVM根据对象在内存中的特征(地址值),通过哈希算法计算出的一个结果。

  • Hash,一般翻译做“散列”,也可以音译为“哈希”,就是把任意长度的数据输入, 通过散列算法,变换成固定长度的输出,该输出就是散列值。 ( x –> f –> = f(x) )
  • 一个任意长度的输入转为一个固定长度的输出,是一种压缩映射,也就是说,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值。

注意:通常情况下我们认为Object中hashCode方法返回的是对象的内存地址值,但实际上并不是。

引用变量的hashCode值:

  • 两个引用变量指向同一个对象,则他们的hashCode值一定相等
  • 两个引用变量的hashCode值相同,则它们有可能指向同一个对象,也可能指向不同对象(散列空间有限)
  • 两个引用变量的hashCode值不同,则他们肯定不可能指向同一个对象

String类

字符串String,是程序中使用最多的一种数据,JVM在内存中专门设置了一 块区域(字符串常量池),来提高字字符串对象的使用效率。

概述

创建字符串对象,和其他普通对象一样,会占用计算机的资源(时间和空间),作为最常用的数据类型,大量频繁的创建字符串对象,会极大地影响程序的性能。

JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化

  • 为字符串开辟一个字符串常量池,类似于缓存区
  • 创建字符串常量时,首先会检查字符串常量池中是否存在该字符串如果存在该字符串,则返回该实例的引用如果不存在,则实例化创建该字符串,并放入池中。

常量池

在Java中,String常量池是一块特殊的内存区域,用于存储字符串常量。String常量池的设计目的是为了节省内存和提高性能。

JDK8及之后的版本中,字符串常量池的位置与其他对象的存储位置,都位于堆内存中。这样做的好处是,字符串常量池的大小可以根据需要进行调整,并且可以享受到垃圾回收器对堆内存的优化

Java将字符串放入String常量池的方式:

  • 直接赋值:通过直接赋值方式创建字符串常量会被放入常量池中
1
String str = "Hello";
  • 调用String类提供intern()方法:可以将字符串对象放入常量池中,并返回常量池中的引用
1
String str = new String("World").intern();

注意:通过new关键字创建的字符串对象不会放入常量池中,而是在堆内存中创建一个新的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
// 常量优化机制:"a" 和 "b"都是字面值常量,借助 + 连接,其结果 "ab" 也被当作常量
String s3 = "a" + "b";
String s4 = "ab";
System.out.println(s3.equals(s4)); // true
System.out.println(s3 == s4); // true
System.out.println("-------------");
String s5 = s1 + s2;
System.out.println(s4.equals(s5)); // true
System.out.println(s4 == s5); // false
System.out.println("-------------");
String s6 = (s1 + s2).intern(); // 因为s4已经把"ab"放入到常量池了所以s6直接拿即可
System.out.println(s4.equals(s6)); // true
System.out.println(s4 == s6); // true
}

下面解释一下String str = “abc”底层的实现查看JDK8官方文档得

注意事项:

  • 使用 + 拼接多个字符串常量,拼接的结果仍旧是字符串常量
  • 如果结果字符串常量在常量池中不存在,则Java会将其放入到字符串常量池中
1
2
3
4
5
6
7
8
//final常量测试
public static void main(String[] args) {
String str = "ab";
//final修饰的String常量
final String str1 = "a"; // final修饰 str1的引用值无法被修改了,指向了常量池,str1相当于常量了
String str2 = str1 + "b";
System.out.println(str == str2); // true
}

扩展

这里着重讲解一下String.intern()这个方法

  • 第一个等式
1
2
String s1 = "中" + new String("国");
System.out.println(s1.intern() == s1); // true
  • 第二个等式
1
2
String s2 = new String("ja") + new String("va");
System.out.println(s2.intern() == s2);

综上所述,java这个字符串早就已经在字符串常量池当中了,故s2.intern() == s2结果为false,原因是一个地址在堆区,一个地址字符串常量池当中。

枚举

枚举类概述

枚举,是JDK1.5引入的新特性,可以通过关键字 enum 来定义枚举类,本质上也是一个类

  • Java中的类,从语法上来说,可以创建无数个对象
  • Java特殊类,其所能创建的对象个数是固定的
1
2
3
4
5
6
7
8
9
10
public class Test071_EnumBasic {
public static void main(String[] args) {
Gender g1 = Gender.MALE;
Gender g2 = Gender.FEMALE;
}
}

enum Gender {
MALE, FEMALE
}

反编译命令:javap -p Gender.class

结合上图我们可知:

  • 枚举类Gender本质上是一个final修饰的类,不可以被继承
  • 枚举类默认继承java.lang.Enum这个抽象泛型类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package java.lang;
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable {
//枚举元素名
private final String name;
public final String name() {
return name;
}
//枚举元素编号,从0开始
private final int ordinal;
public final int ordinal() {
return ordinal;
}
//...省略
}
  • 枚举元素,本质上是枚举类对象,且由static和final修饰
  • 枚举类提供私有构造器,我们在类外不能主动创建枚举类对象
  • 枚举类中可以包含public static 静态方法

枚举基本定义

1
2
3
[修饰符] enum 枚举类名 {
枚举元素1,枚举元素2,...枚举元素n;
}

案例展示: 定义枚举类Week,要求包含多个枚举元素

1
2
3
4
5
6
7
8
9
10
//枚举类基本定义
enum Week {
//枚举元素必须写在第一行,如果有多个的话,用逗号','隔开,
//最后用分号';'结束
//如果';'后面没有其他内容的话,';'可以省略,但不建议省略
MON, TUE, WED; //WED()
//WED和WED()效果一样,含义为:执行枚举类默认构造器去实例化枚举元素对象
//源代码类似:public static final Week WED = new Week();
//注意:用户没有提供构造器,系统会提供默认的构造器 private Week(){}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
//1.枚举元素引用格式:枚举类名.枚举元素名;
//注意枚举类名不能省略
Week w1 = Week.MON;
Week w2 = Week.TUE;
Week w3 = Week.WED;
//2.输出枚举对象,默认输出枚举元素名
System.out.println(w1);
System.out.println(w2.toString());
System.out.println("-----------");
//3.获取枚举元素名:跟元素名一样的同名字符串
System.out.println(w3.name());
//4.获取枚举元素编号:从0开始,逐个加1
System.out.println(w1.ordinal()); //0
System.out.println(w2.ordinal()); //1
System.out.println(w3.ordinal()); //2
}

构造方法定义

包含数据成员、构造方法的枚举类.

定义格式:

1
2
3
4
5
6
7
8
9
10
11
[修饰符] enum 枚举类名 {
枚举元素1(实际参数列表), ...枚举元素n(实际参数列表);
//枚举类数据成员和成员方法,可以包含多个
[修饰符] 数据类型 数据成员名;
[修饰符] 返回值类型 成员方法名(形参列表) {
方法体实现;
}
//枚举类构造方法,可以包含多个
//注意,必须使用private进行修饰
private 构造方法;
}

案例展示: 定义枚举类Week2,要求包含私有数据成员desc和构造方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum Week {
MON, TUE(), WED("星期三");

Week(String string) {
}

Week() {
}

private String desc;

public String getDesc() {
return desc;
}

public void setDesc(String desc) {
this.desc = desc;
}

}

抽象方法定义

包含抽象方法的枚举类

1
2
3
4
5
6
7
8
9
10
[修饰符] enum 枚举类名 {
枚举元素1(实参列表) {
重写所有抽象方法;
}, ...枚举元素n(实参列表) {
重写所有抽象方法;
};
//可以包含多个抽象方法
抽象方法声明;
//数据成员、成员方法及构造方法略...
}

案例展示: 定义枚举类Week3,要求包含抽象方法show()。

  • 每一个枚举元素都要实现抽象方法
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
package com.briup.chap07.blog;

public class Test072_Define01 {
public static void main(String[] args) {
Week mon = Week.MON;
System.out.println(mon);
mon.show();
System.out.println("-------------------");
Week tue = Week.TUE;
System.out.println(tue);
tue.show();
}
}

interface ShowAble {
void show();
}

enum Week implements ShowAble{
// 注意:包含抽象方法的枚举类是抽象类,不能直接实例化对象
// 所以定义枚举类元素(所有)时候,一定要重写抽象方法
// 注意:必须在所有的枚举元素定义中,重写所有抽象方法
// MON {
MON() {
// 在枚举元素中重写抽象方法
@Override
public void show() {
System.out.println("in show, MON: 周一");
}
},
TUE("星期二") {
// 注意,每个枚举元素中都要重写重写方法,且要重写所有的抽象方法
@Override
public void show() {
System.out.println("in show, TUE: " + this.getDesc());
}
};

// 枚举类数据成员及get方法
private String desc;

public String getDesc() {
return desc;
}

// 枚举类自定义构造方法
private Week() {
}

private Week(String desc) {
this.desc = desc;
}

// 枚举类 包含的 抽象方法(可以0或多个)
public abstract void show();

}
  • 通过接口来实现抽象方法,不需要每个元素来实现抽象方法
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 com.briup.chap07.blog;

public class Test072_Define01 {
public static void main(String[] args) {
Week mon = Week.MON;
System.out.println(mon);
mon.show();
System.out.println("-------------------");
Week tue = Week.TUE;
System.out.println(tue);
tue.show();
}
}

interface ShowAble {
void show();
}

enum Week implements ShowAble {
MON(), TUE("星期二");

// 枚举类数据成员及get方法
private String desc;

public String getDesc() {
return desc;
}

// 枚举类自定义构造方法
private Week() {
}

private Week(String desc) {
this.desc = desc;
}

// 枚举类 包含的 抽象方法(可以0或多个)
public void show() {
System.out.println("I am interface abstract method show()");
}

}

枚举总结

在实际项目开发中,我们定义枚举类型,大多数情况下使用最基本的定义方式, 偶尔会添加属性、方法和构造方法,并不会写的那么复杂。

枚举类注意事项:

  • 定义枚举类要使用关键字 enum
  • 所有枚举类都是java.lang.Enum的子类
  • 枚举类的第一行上必须是枚举元素(枚举项)
  • 最后一个枚举项后分号是可以省略的,但是 ;后面还有其他有效代码,这个分号就不能省略,建议不要省略
  • 用户如果不提供构造方法,系统会提供默认的构造方法:private 枚举类() {}
  • 用户可以通过构造方法,但必须用private修饰,同时系统不再提供默认构造方法
  • 枚举类也可以由抽象方法,但是枚举元素必须重写所有抽象方法

❤️❤️❤️忙碌的敲代码也不要忘了浪漫鸭!

路漫漫其修远昔,吾将上下而求索。💪