# 一、数组
# 单个数组内存分配图
# 多个数组内存分配图
# 多个数组指向相同地址
这种情况下,多个数组指向同一个地址值。
中间一行的赋值操作是将arr的地址值赋值给arr2,如果这个时候针对arr2进行操作,那么也就相当于是对arr进行操作,本质上指向的是同一个数组。所以无论操作arr还是arr2,结果上没有本质上的区别。
# 数组空指针异常
如果数组被赋值为null,那么将找不到数组本身存放的堆内存地址。再次使用的时候会报错:空指针异常
# 二、内部类、抽象类、包装类、修饰符
# 内部类
在一个类中定义一个类,类中被定义的类就是内部类
# 内部类的访问特点
内部类可以直接访问外部类的成员,包括私有
外部类要访问内部类的成员,必须创建对象
public class Outer {
private int num = 20;
public class Inner {
public void show() {
System.out.println(num);
}
}
private void method() {
Inner inner = new Inner();
inner.show();
}
}
2
3
4
5
6
7
8
9
10
11
12
# 成员内部类
根据内部类的位置不同,可以分为两种:
- 在类的成员位置:成员内部类
- 在类的局部位置:局部内部类
成员内部类如何使用呢?两种方式:
一、将内部类的权限名定义为public,之后创建内部类
public class Outer {
private int num = 20;
public class Inner {
public void show() {
System.out.println(num);
}
}
}
2
3
4
5
6
7
8
public static void main(String[] args) {
// 创建对象调用内部类方法
Outer.Inner oi = new Outer().new Inner();
oi.show();
}
2
3
4
5
二、如果Inner内部类的权限名不是public,则上述方法失效,那么如何调用呢?
在外部类内创建新的方法,创建内部类,调用方法;外界直接创建外部类,并调用该方法即可
public class Outer {
private int num = 20;
private class Inner {
public void show() {
System.out.println(num);
}
}
public void method() {
Inner i = new Inner();
i.show();
}
}
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
// 创建对象调用内部类
/*Outer.Inner oi = new Outer().new Inner();
oi.show();*/
Outer o = new Outer();
o.method();
}
2
3
4
5
6
7
# 局部内部类
局部内部类就是在方法体中的类,所以外界是无法使用的,需要在方法中创建该局部内部类的对象,通过调用对象内部的方法使用
该类可以访问外部的成员,也可以访问方法内的局部变量
public class Outer {
private int num = 10;
public void method() {
class Inner {
int num2 = 20;
public void show() {
System.out.println(num);
System.out.println(num2);
}
}
// 直接在方法内部创建对象调用局部内部类的方法
Inner i = new Inner();
i.show();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
Outer o = new Outer();
o.method();
}
2
3
4
# 匿名内部类
前提:存在一个类或者一个接口,这里的类可以是具体类也可以是抽象类
格式:
new class/interface() {
// Override method()
};
2
3
本质是***一个继承了该类或实现了该接口的子类匿名对象***
步骤一:有一个类或者接口
public interface Inter {
void show();
}
2
3
步骤二:创建相关的类
public class Outer {
public void method() {
/*new Inter() {
@Override
public void show() {
System.out.println("匿名内部类方法执行");
}
};
这样写仅仅是个对象,下面的写法才是对象调用方法:
new Inter() {
@Override
public void show() {
System.out.println("匿名内部类方法执行");
}
}.show();*/
// 由于该匿名内部类实现的是 Inter 接口,我们可以用接口类型来接受这个匿名内部类
Inter i = new Inter() {
@Override
public void show() {
System.out.println("匿名内部类方法执行");
}
};
i.show();
i.show();
}
}
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
步骤三:测试
public class Test {
public static void main(String[] args) {
Outer o = new Outer();
o.method();
}
}
2
3
4
5
6
输出结果:
匿名内部类方法执行 匿名内部类方法执行
# 匿名内部类在开发中的使用
仅使用一次,创建接口操作类的对象,调用接口操作方法,方法的参数是接口
不想创建接口实现类的情况,并且只想用一次,就可以使用匿名内部类
# 抽象类
Java中,没有方法体的方法应该被定义为抽象方法;类中如果有抽象方法,则应该定义为抽象类
注意事项:
- 抽象类中的方法不一定是抽象方法,但是抽象方法所属的类一定要是抽象类,抽象类中一定存在抽象方法
- 抽象类中的子类要么是抽象类,要么重写抽象类中的所有抽象方法
- 抽象类不能实例化,但是抽象类可以通过子类对象进行实例化,这叫抽象类多态
# 抽象类的成员特点
抽象类中可有成员变量、成员方法、构造方法
- 成员变量
- 可以是变量,也可以是常量
- 成员方法
- 可以有抽象方法:限定子类必须完成某些动作
- 可以有非抽象方法:提高代码的复用性
- 构造方法
- 有构造方法,但是不能实例化
- 抽象方法的实例化是通过子类的对象进行实例化的,子类对象对于父类数据的初始化要使用到这些构造方法
public abstract class Animal {
// 抽象类中可以包含成员变量
private int age = 20;
private final String city = "北京";
// 因为抽象类是通过子类对象进行实例化的,所以子类对象在使用构造方法创建时,
// 会隐式的调用父类的构造方法,也就是抽象类的构造方法
public Animal() {}
public Animal(int age) {
this.age = age;
}
public void show() {
age = 40;
System.out.println(age);
System.out.println(city);
}
/*public void eat() {
System.out.println("吃东西");
}*/
// 抽象方法
public abstract void eat();
// 抽象类中可以有具体的方法
public void sleep() {
System.out.println("睡觉");
}
}
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
# 包装类
基本数据类型使用虽然非常方便,但是没有对应的方法来操作这些数据。所以我们可以使用包装类将这些基本数据类型进行一定的封装,把基本类型的数据包装起来,这就是包装类。
在包装类中可以定义一些方法,用来操作基本类型的数据。
# 装箱与拆箱
# 修饰符
Java中的修饰符分为两大类:权限修饰符、状态修饰符
# 权限修饰符
权限修饰符,修饰的是访问的权限,指的是在同一个module中的不同类中的访问权限
# 状态修饰符
# final(最终态)
可以修饰成员方法、成员变量、类
- final修饰方法,表明该方法是最终方法,不能被重写
- final修饰变量,表明该变量是最终变量,不能被再次赋值
- final修饰类,表明该类是最终类,不能被继承
final修饰局部变量:
- final修饰基本数据类型变量,变量的数据值不能发生改变
- final修饰引用数据类型变量,变量的地址值不能发生改变,但是地址里的内容是可以发生改变的
# static(静态)
可以修饰成员方法、成员变量
- 被类的所有对象共享——这也是我们判断是否使用static关键字的条件
- 可以通过类名.变量名调用(也可以使用对象名调用),推荐使用类名调用
- 静态成员方法只能访问静态成员
# 三、继承、多态
# 继承
继承是面向对象三大特征之一,可以使得子类具有父类的属性和方法,还可以在子类中重新定义,追加属性和方法
# 继承的利弊
- 优点:
- 提高了代码的复用性(多个类相同的成员可以放到同一个类中)
- 提高了代码的维护性(如果方法的代码需要修改,修改一处即可)
- 缺点:
- 继承让类与类之间产生了关系,类的耦合性增强了,当父类发生变化时,子类实现也不得不跟着变化,削弱了子类的独立性
# 继承使用的情况
什么时候使用继承?
当类A是类B的”一种/一个“时,就可以使用继承关系。
# 继承中成员变量访问
- 在子类方法中访问变量
- 子类局部范围找
- 子类成员范围找
- 父类成员范围找
- 如果都没有就报错(不考虑父类的父类)
# super与this关键字的使用
- super:代表父类存储空间的标识(可以理解为父类对象引用)
- this:代表本类对象的引用
# 继承中成员方法的访问
继承中的成员方法的访问:(子类访问成员方法)
- 子类成员范围找
- 父类成员范围找
- 如果找不到就报错(不考虑父类的父类)
public static void main(String[] args) {
Zi zi = new Zi();
// 调用子类成员方法
zi.method();
// 调用子类和父类中的重名无参方法
// 真正调用的是子类中的同名方法
zi.show();
// 调用父类中的方法,需要在子类中的同名方法中添加 super.show();
}
2
3
4
5
6
7
8
9
# 继承中构造方法的访问特点
继承中,关于构造方法的访问:
子类中所有的构造方法都会默认访问父类中的无参构造方法
原因:子类继承自父类,在子类调用父类时可能会用到父类的数据,所以在子类进行初始化的时候需要先对父类进行初始化操作
因为子类会继承父类的数据,可能还会使用父类的数据。所以在子类初始化之前需要先完成父类数据的初始化
每一个子类构造方法的第一句默认都是
super();
如果父类中没有无参构造方法,只有带参构造方法,解决方法:
- 通过使用super关键字显式的调用父类的带参构造方法
- 在父类中自己提供一个无参构造方法(推荐使用)
public static void main(String[] args) {
/*
* 父类无参构造方法被调用
* 子类无参构造方法被调用
* */
Zi z1 = new Zi();
/*
* 父类无参构造方法被调用
* 子类带参构造方法被调用
* */
Zi z2 = new Zi(20);
}
2
3
4
5
6
7
8
9
10
11
12
# super中的内存图
# 方法重写
子类中出现了和父类中一样的方法声明
当子类中需要父类的功能,而功能主体子类有自己特有内容,可以重写父类中的方法。这样既沿袭了父类中的功能,又定义了子类特有的功能
方法重写时最好添加@Override
注解,可以帮忙检查方法重写的方法声明是否正确
注意事项
- 父类中的私有方法子类不能重写(父类中的私有成员子类是不能被继承的)
- 子类重写父类方法,子类的访问权限不能更低。例如:父类成员方法为默认,子类重写方法权限为默认或比默认更高(protected,public)
- 方法访问权限:public > protected > 默认 > private
# 继承的注意事项
Java中的继承只支持单继承,不支持多继承(一个类继承自多个类,不允许)
继承支持多级继承,子类继承自父类,父类继承自父类的父类(爷爷类)这样的继承是合法的
# 多态
多态定义的时候:左父右子
# 多态中成员访问
成员变量:编译看左边,执行看左边
成员方法:编译看左边,执行看右边
成员方法和成员变量执行不同的原因:因为成员方法有重写,而成员变量没有
底层上的解释就是成员变量属于前期绑定(静态绑定,程序编译期的绑定),成员方法属于后期绑定(动态绑定,程序运行期的绑定)。
重载属于前期绑定,重写属于后期绑定。
也就是说:多态的编译是否能通过,要看父类中是否有相关的变量和方法;执行则要看是变量还是方法
# 多态的利弊
多态的好处:提高了程序的扩展性
多态的弊端:不能使用子类的特有功能
定义多态方法的时候,使用父类型作为参数,使用具体的子类型进行操作
实际使用如下:
①创建Animal类
public class Animal {
public void eat() {
System.out.println("动物吃东西");
}
}
2
3
4
5
②创建Dog类和Cat类
public class Cat extends Animal{
public void eat() {
System.out.println("猫吃老鼠");
}
}
2
3
4
5
public class Dog extends Animal{
public void eat() {
System.out.println("狗吃骨头");
}
public void gatekeeper() {
System.out.println("狗看门");
}
}
2
3
4
5
6
7
8
9
③创建测试主类
public class AnimalDemo {
public static void main(String[] args) {
Animal a = new Cat();
a.eat();
Animal b = new Dog();
// b.gatekeeper();
b.eat();
}
}
2
3
4
5
6
7
8
9
10
这个时候我们调用gatekeeper方法则会报错,因为gatekeeper方法是Dog类独有的方法。多态的弊端此时体现出来了:因为在Animal父类中没有定义gatekeeper方法,那么在使用多态的时候就不能调用到子类的独有方法。
解决方法:①在Animal类中添加Dog的特有方法,那这样Cat类也能够调用gatekeeper方法,本质上二者相悖了。
public void gatekeeper() {
System.out.println("动物看家护院");
}
2
3
所以说,多态的弊端就是不能调用子类的特有方法。
②向下转型,将Animal类定义的时候的b对象(Dog)转型为Dog本身的类型。这样转型之后其实也与多态定义的时候的方法不相符,违背了多态本身的定义。
((Dog) b).gatekeeper();
# 四、接口、泛型
# 接口Interface
接口就是一种公共的规范标准,Java中的接口更多体现在对行为的抽象
接口使用interface关键字来创建;接口的使用是通过类来实现该接口实现的
public interface Jumping {
public abstract void jump();
}
2
3
接口不能实例化,要想使用需要通过一个类来实现该接口,通过实现类对象来实例化(与抽象类在这一方面类似),叫做接口多态
# 接口的成员特点
- 成员变量
- 只能是常量,默认由public static final修饰,不能进行二次赋值
- 成员方法
- 只能是抽象方法,默认由public abstract修饰,不能是非抽象方法
- 构造方法
- 接口中没有构造方法,因为接口主要是对行为进行抽象,没有具体存在
- 一个类如果没有父类,则默认继承自Object类
public interface Inter {
public int num = 20; // 接口中的成员变量默认是被 static final 修饰
public final int num2 = 30;
public static final int num3 = 40;
public abstract void method();
void show();
/* 接口中不能有构造方法和非抽象方法的
public Inter() {}
public void show() {}*/
}
2
3
4
5
6
7
8
9
10
11
12
# 泛型Generic
泛型的使用可以使一些集合的使用中可能出现的类型错误,由运行期错误转换为编译期错误,编码的时候更加安全。
①把运行期间的问题提前到了编译期
②避免了强制类型转换
# 泛型类
定义格式:
- 修饰符 class 类名<类型> {}
- 例如:
public class Generic<T> {}
- 此处 T 可以随便写为任意标识,常见的比如 T 、E 、K 、V 等形式的参数常用于表示泛型
使用步骤:
①创建泛型类Generic类
public class Generic<T> {
private T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
}
2
3
4
5
6
7
8
9
10
11
②使用泛型类
public class GenericDemo {
public static void main(String[] args) {
Student student = new Student();
student.setStuName("张三");
student.setStuAge(18);
System.out.println(student.getStuName());
System.out.println(student.getStuAge());
Generic<String> g1 = new Generic<String>();
g1.setT("李四");
System.out.println(g1.getT());
Generic<Integer> g2 = new Generic<Integer>();
g2.setT(20);
System.out.println(g2.getT());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
对比普通类(student),泛型类在使用的时候更加简便,不需要过多的创建set和get方法
# 泛型方法
在上述的例子中,我们使用泛型方法需要多次创建新的对象,使用中还是比较繁琐。为了能够创建一次对象,多次调用不同的参数的同一个方法,我们可以使用泛型方法。
定义格式如下
public class GenericFunction {
public <T> void show(T t) {
System.out.println(t);
}
}
2
3
4
5
使用方法
GenericFunction g = new GenericFunction();
g.show("雨下一整晚Real");
g.show(20);
g.show(true);
2
3
4
打印输出结果如下所示:
雨下一整晚Real 20 true
# 泛型接口
我们定义一个泛型接口
public interface GeneticInterface<T> {
void show(T t);
}
2
3
定义接口的实现类
public class GenericInterfaceImpl<T> implements GeneticInterface<T>{
@Override
public void show(T t) {
System.out.println(t);
}
}
2
3
4
5
6
测试运行类
GeneticInterface<String> geneticInterface1 = new GenericInterfaceImpl<String>();
geneticInterface1.show("Real");
GeneticInterface<Integer> geneticInterface2 = new GenericInterfaceImpl<Integer>();
geneticInterface2.show(21);
GeneticInterface<Boolean> geneticInterface3 = new GenericInterfaceImpl<Boolean>();
geneticInterface3.show(true);
2
3
4
5
6
7
8
运行结果如下
Real 21 true
# 类型通配符
public class GenericDemo {
public static void main(String[] args) {
List<?> list1 = new ArrayList<Object>();
List<?> list2 = new ArrayList<Number>();
List<?> list3 = new ArrayList<Integer>();
/*这三个类是继承关系,按照继承顺序编写的*/
System.out.println("--------");
/*类型通配符上限*/
// List<? extends Number> list4 = new ArrayList<Object>();
List<? extends Number> list5 = new ArrayList<Number>();
List<? extends Integer> list6 = new ArrayList<Integer>();
/*类型通配符下限*/
List<? super Number> list7 = new ArrayList<Object>();
List<? super Number> list8 = new ArrayList<Number>();
// List<? super Number> list9 = new ArrayList<Integer>();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
在上述的代码中,添加注释的行是错误的。根据上限和下限的定义,我们可以得出super和extends的使用。
# 可变参数
要想实现多个数字之和,这种方法的实现,需要用到可变参数
如果为每一个数量的数求和编写一个方法,那么工作量将会变得非常大。这个时候我们就可以用到可变参数
使用如下:
public static void main(String[] args) {
System.out.println(sum(10, 20));
System.out.println(sum(10, 20, 30));
System.out.println(sum(10, 20, 30, 40));
}
static int sum(int... a) {
int sum = 0;
for (int i : a) {
sum += i;
}
return sum;
}
2
3
4
5
6
7
8
9
10
11
12
13
其中,a是一个数组类型的数据。我们求和的时候直接遍历数组求和即可。
如果sum方法有多个参数,那么可变参数应该放在后面
如果调换二者的顺序,则不能通过编译。
# 可变参数的使用
public static void main(String[] args) {
List<String> list = Arrays.asList("Hello", "World", "java");
// UnsupportedOperationException
// list.add("java EE");
// list.remove("java");
list.set(2, "java EE");
System.out.println(list);
List<String> stringList = List.of("Hello", "World", "java");
// UnsupportedOperationException
// stringList.add("java EE");
// stringList.remove("java");
// stringList.set(2, "java EE");
System.out.println(stringList);
// set集合不允许有重复元素
Set<String> set = Set.of("Hello", "World", "java");
// UnsupportedOperationException
// set.add("java EE");
// set.remove("java");
System.out.println(set);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
注释掉的部分是不支持的内容,不允许的部分。