Java基本功-面向对象

知识点:类与对象,作用域,构造函数,继承,接口,泛型,反射,日志

1. 类与对象

面向对象程序设计OOP

是构造对象的模板,java编写的所有代码都位于类内部。

对象 通过创建类的实例生成类对象

1.1. 类关系

聚合 uses-a

依赖 has-a

继承 is-a

1.2. 面向对象vs面向过程

1.2.1. 面向过程

通过一系列的算法来求解问题。确定了这些算法就需要考虑存储数据方式,即算法+数据结构=程序,算法是第一位的

面向过程性能比面向对象高。 因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix 等一般采用面向过程开发。但是,面向过程没有面向对象易维护、易复用、易扩展。

1.2.2. 面向对象

将数据放在第一位,然后考虑操作数据的算法

面向对象易维护、易复用、易扩展。 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,面向对象性能比面向过程低

面向过程也需要分配内存,计算内存偏移量,Java 性能差的主要原因并不是因为它是面向对象语言,而是 Java 是半编译语言,最终的执行代码并不是可以直接被 CPU 执行的二进制机器码。

而面向过程语言大多都是直接编译成机械码在电脑上执行,并且其它一些面向过程的脚本语言性能也并不一定比 Java 好。

1.3. 自定义

public class A{}

在一个源文件中,只能有一个公有类,但可以有任意数目的非公有类当一个源文件(A.java)中包含多个类时,javac A.java java编译器会自动编译文件中的所有类

1.4. 构造器constructor

构造器实际是一个方法,主要作用是完成对类对象的初始化工作。

1.4.1. 构造器定义

  • 方法名与类名相同
  • 类没有构造函数时,编译器在把源代码编译成字节码的过程中会提供一个没有参数默认的构造函数,但该构造函数不执行任何代码
  • 类可以有一或多个构造器 – 重载,但不能被重写
  • 构造器可以有任意个参数
  • 构造器没有返回值,甚至没有void
  • 总是伴随着new操作一起调用,且必须由系统调用。在对象实例化时被调用,且仅调用一次
  • 构造函数不能被继承,不能被覆盖,可以被重载
  • 子类可以通过super关键字来显式调用父类的构造函数,当父类没有提供无参数的构造函数时,子类的构造函数必须显式地调用父类的构造函数,父类提供了无参数构造函数时,子类的构造函数就可以不显式地调用父类的构造函数,编译器默认调用父类的无参数构造函数。有父类时,实例化子类会先执行父类的构造函数。
  • 父类,子类都没有定义构造函数时,编译器会为父类生成一个默认的无参数构造函数,给子类也生成一个无参数构造函数。默认构造器的修饰符只跟当前类的修饰符有关
  • 普通方法可以与构造函数有相同的方法名

1.4.2. 隐式参数this

  1. 表示正在初始化的对象;
  2. 调用同类另一个构造器this()

1.4.3. 隐式参数super

在子类的方法或构造器中,通过使用”super.属性”或”super.方法”的方式,显式的调用父类中声明的属性火方法。通常情况下,习惯省略”super.”。

当子类和父类中定义了同名的属性时,必须显式使用”super.”调用父类属性。

  1. 调用超类方法
  2. 调用超类构造器
  3. 调用超类属性

1.4.4. 创建对象new

对象的创建通过在类构造器前加上new操作符

1
2
3
4
5
6
//创建Date对象
Date d1 = new Date();
//变量定义,并不是Date对象
Date d2;
//将d1所指对象地址复制给d2,两者指向同一片地址
d2 = d1;

1.5. 对象

一类事物的集体实现。

对象中的数据称为实例域,操纵数据的过程称为方法

创建对象方式

  1. new 实例化
  2. 反射机制 newInstance()
  3. clone()创建一个对象
  4. 反序列化创建对象

创建对象过程:

  1. 分配对象空间,并将对象成员变量初始化为0或空
  2. 执行属性值的显式初始化
  3. 执行构造方法
  4. 返回对象的地址给相关变量

2. 变量作用域

java允许局部变量和成员变量同名,局部变量会覆盖成员变量。

2.1. 成员变量

从语法形式上看,成员变量属于类,可以被public,private,static修饰,也可以被final修饰。

类的成员变量的作用范围同类的实例化对象的作用范围,类实例化时,成员变量就会在内存中分配空间并初始化,直到这个实例化对象的生命周期结束,成员变量的生命周期才结束。

static静态变量不依赖于特定的实例,而是被所有实例所共享,只要一个类被加载,jvm就会给类静态变量分配存储空间,通过 [类名.变量] 来访问静态变量。

2.2. 局部变量

局部变量不能被访问控制修饰符及static修饰,可以被final修饰

局部变量的作用域与可见性为它所在的花括号内

2.3. 成员变量 vs 局部变量

不同点 成员变量 局部变量
位置不同 方法外部,直接写在类中 方法内
作用范围不同 整个类可以通用 方法内通用
默认值不同 无赋值可以有默认值 必须赋值
生命周期不同 随对象创建和被垃圾回收 方法执行周期
内存位置不同 用static修饰的属于类,没有用static修饰的属于实例,对象存在于堆内存 栈内存
修饰符 public, private,static,final final

2.4. 访问权限

private,protected不能修饰类,public,abstract,final才能用来修饰类

作用域与可见性 当前类 同一个包 不同包子类 不同工程类
public ☑️ ☑️ ☑️ ☑️
protected ☑️ ☑️ ☑️
default ☑️ ☑️
private ☑️

关于protected的两个细节:

  1. 若父类和子类在同一个包中,子类可访问父类的protected,也可以访问父类对象的protected成员
  2. 若子类和父类不在同一个包中,子类可以访问父类的protected成员,不能访问父类对象的protected成员

3. 值传递

3.1. 形参&实参

方法的定义可能会用到 参数(有参的方法),参数在程序语言中分为:

  • 实参(实际参数) :用于传递给函数/方法的参数,必须有确定的值。
  • 形参(形式参数) :用于定义函数/方法,接收实参,不需要有确定的值。
1
2
3
4
5
6
7
8
String hello = "Hello!";
// hello 为实参
sayHello(hello);
// str 为形参
void sayHello(String str) {
System.out.println(str);
}

3.2. 值传递&引用传递

程序设计语言将实参传递给方法(或函数)的方式分为两种:

  • 值传递 :方法接收的是实参值的拷贝,会创建副本。
  • 引用传递 :方法接收的直接是实参所引用的对象在堆中的地址,不会创建副本,对形参的修改将影响到实参。

很多程序设计语言(比如 C++、 Pascal )提供了两种参数传递的方式,不过,在 Java 中只有值传递。

java传参数分为基本数据类型传递,引用类型传递。都是以值传递的,传递的是值的副本。对象传递的值是指向的地址值

传递基本数据类型参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static void change(int a){
a=2;
}
public static void main(String[] args){
int x=1;
System.out.println(x); //1
change(x);
/*
a只存在于change方法块内,
只作为x的值拷贝,副本,并不影响x本身,
离开方法体a不再使用
*/
System.out.println(x); //1
}

传递引用类型参数

变量t拷贝的是test对象的指向地址。对象中x的值改变了就是改变了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test {
private int x=1;
public int getX() { return x; }
public void setX(int x) { this.x = x; }
private static void change(Test t){
t.setX(3);
}
public static void main(String[] args) {
Test test = new Test();
test.setX(2);
System.out.println(test.getX()); //2

change(test);
System.out.println(test.getX()); //3
}
}

4. 时间类

时间是从1970年1月1日00:00:00开始的毫秒数

4.1. java.util.Date

1
2
3
4
5
6
7
8
9
10
11
12
13
GregorianCalendar g = new GregorianCalendar();
g.get(Calendar.MONTH); //月
g.get(Calendar.DAY_OF_MONTH);//日
g.get(Calendar.DAY_OF_WEEK);//周

//改变状态
g.set(Calendar.MONTH,Calendar.APRIL);

Date time=g.getTime();

Date d = new Date();
SimpleDateFormat sm = new SimpleDateFormat("yyyy-MM-dd");
String str = sm.format(d);

Calendar,Date面临的问题:

  • 可变性:像日期和时间类应该是不可变的

  • 偏移性:Date中的年份是从1900开始的,而月份从0开始

    1
    2
    3
    Date date = new Date(2002-1900,9,8);
    //Tue Oct 08 00:00:00 CST 2002
    System.out.println(date);
  • 格式化:格式化只对Date有用,Calendar则不行

  • 它们不是线程安全的,不能处理闰秒

4.2. java.time.LocalDate

now():获取当前的日期,时间,日期+时间

of():得到特定时间对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//静态工厂方法构建对象
LocalDate date = LocalDate.now();
//2021-01-21
System.out.println(date);

LocalTime time = LocalTime.now();
//20:23:41.133
System.out.println(time);

LocalDateTime dateTime = LocalDateTime.now();
//2021-01-21T20:23:41.134
System.out.println(dateTime);

//构建特定日期对象
dateTime=LocalDateTime.of(2002,9,8,14,25);
//2002-09-08T14:25
System.out.println(dateTime);

4.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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Test
void testSystem(){
//当前日期对象
LocalDate date = LocalDate.now();
date = LocalDate.of(2019,9,13);
//当前日期月份 - 9
int month = date.getMonthValue();
//当前日期的几号 - 13
int today = date.getDayOfMonth();

//变为当前月的1号
date = date.minusDays(today-1);
//当前日期是星期几
DayOfWeek weekDays = date.getDayOfWeek();
//星期几对应数值 1-Monday 7-sunday
int value = weekDays.getValue() % 7;

System.out.println("Sun Mon Tue Wed Thu Fri Sat");
//当前日期之前的日子用空格代替
for(int i = 0; i < value ; i++){
System.out.printf(" ");
}
//当加到下一月时停止
while (date.getMonthValue() == month){
//输出当前日
System.out.printf("%3d",date.getDayOfMonth());
//对比当前日期和保存的日期值
if(date.getDayOfMonth()==today){
System.out.print("*");
}else{
System.out.print(" ");
}
//日期+1
date = date.plusDays(1);
if (date.getDayOfWeek().getValue()==7){
System.out.println();
}
}
if (date.getDayOfWeek().getValue()!=7){
System.out.println();
}

}

输出

1
2
3
4
5
6
Sun Mon Tue Wed Thu Fri Sat
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

5. 继承

extends关键字

提高代码复用功能,子类继承父类。

Java不支持多继承,但接口可以多继承父接口。

子类只能继承父类的非私有(public,protected)成员变量与方法。

子类与父类有相同的成员变量时,子类的成员变量会覆盖父类的成员变量,而不是继承。有相同的函数时,子类也会覆盖父类的方法,而不是继承。子类不能继承,显式调用父类的构造函数,子类可通过super()进行调用

5.1. 重载 vs 重写

重载 重写
位置 发生在同一个类中 父类的子类中
定义 同一方法名的参数顺序,个数,类型不同;返回值,访问权限,异常不同并不构成重载 1. 同一方法名,参数相同;
2. 子类返回值,异常类型小于父类,访问权限大于父类
3. 构造方法无法被重写
原理 方法调用 用于多态
发生阶段 编译期 运行期

方法的重写要遵循“两同两小一大”

  • “两同”即方法名相同、形参列表相同;
  • “两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;
  • “一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等

5.2. 多态

当同一个存在作用在不同对象时,会有不同的语义。

编译时多态,运行时多态。

方法的重载,同一个类有多个同名的方法,方法有不同的参数,编译时就可以确定到底调用哪个方法,是一种编译时多态,方法多态性。

方法的覆盖,父类的引用变量可以指向子类的实例对象,在运行时才动态绑定,确定调用哪个方法,是运行时多态。

成员变量无法实现多态。

多态的特点:

  • 对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
  • 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
  • 多态不能调用“只在子类存在但在父类不存在”的方法;
  • 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。

5.3. 动态绑定 c.f(param)

  1. 编译器会一一列举所有c类中名为f的方法和超类中的访问属性为public且名为f的方法,从而得到所有可能被调用的候选方法。
  2. 编译器查看调用方法时提供的参数类型,进行重载解析,匹配参数类型的方法
  3. 若为private,static,final方法则进行静态绑定
  4. 进行方法调用。调用方法时,虚拟机一定选择与x对象最合适的类方法。x为A类型,A的父类为B,都定义了方法f(),优先使用本类中的方法

动态绑定:调用的方法依赖于隐式参数的实际类型

虚拟机预先为每个类创建了一个方法表,列出了所有方法的签名和实际调用的方法

Manager extends Employee,则Manager的方法表为

Manager:

​ getName() -> Employee.getName()

​ getSalary() -> Manager.getSalary()

5.4. 不可变类

一旦创建了这个类的实例就不允许修改它的值,它的成员变量也不能被修改,在Java中所有基本数据类型的包装类是不可变类。

包装类中所有成员变量被private修饰,只提供构造函数,没有set,get方法,所有方法不能被子类覆盖

1
2
3
4
5
6
7
8
9
10
11
public final class Integer extends Number implements Comparable<Integer> {  

private final int value;

public Integer(int value) {
this.value = value;
}
public int intValue() {
return value;
}
}

6. 抽象类 - 模板

abstract关键字。

如果一个类中包含抽象方法,那么这个类必须声明为抽象类。没有抽象方法的抽象类也不能直接创建对象

目的就是不让调用者实例化该类

抽象类中可以包含普通方法和成员变量,但不能被实例化。

仍有未实现的方法,不能调用抽象方法

是一种模块,类声明为abstract,抽象类只能被继承extends,且只能是单继承,抽象强调has-a关系

本质上也是一个父类

子类必须重写父类所有的抽象方法

含有抽象方法的类必须声明为抽象类

可以有构造函数,供子类创建对象,初始化父类成员使用

子类的构造方法中,有默认的super(),需要访问父类的构造方法

7. 接口 - 需求描述

implement关键字

接口可以被实现implements,一个类可以实现多个接口,接口可以多继承接口以实现类多继承的功能,接口只能继承接口,不能继承类,接口强调is-a关系

当多个接口中拥有相同的默认方法时,实现类必须进行方法重写

当实现类拥有接口和父类的相同方法时,优先选择父类的方法

多个父接口当中的默认方法重复,子接口必须进行重写,并且带有default关键字

抽象,接口都不能被实例化,都可以通过多态调用子类,实现类方法

接口的方法默认都是 public abstract的,并且不允许定义为 private 或者 protected。(jdk1.8)

接口的字段默认都是 public static final 的

必须赋初值

是一种规范,类声明为interface,接口中可以包含静态常量,方法(抽象实例方法,类方法,默认方法),内部类(内部接口,枚举)

可以直接用接口.静态方法名 调用

7.1. jdk8新增静态方法

1
2
3
4
5
6
7
8
9
10
interface A{
public static String getStr(){
return "A";
}
}

@Test
void testSystem(){
String str = A.getStr();
}

7.2. jdk8新增默认方法

Jdk1.8+,可有默认方法,解决接口出现新方法定义,影响其所有实现类;

可以被实现类直接调用实现,也可以被实现类重写

方法用default修饰

1
2
3
4
5
6
7
public interface CompareA{
public default void method(String str){
System.out.println(str);
}
}
//调用接口中的默认方法
CompareA.super.method("");

当接口存在继承关系时,若子接口有多个父接口,父接口中拥有同一个默认方法时,需要子接口必须覆盖这个默认方法

7.3. jdk9新增私有方法

1
2
private 返回值 方法名(参数){ 方法体 }
private static 返回值 方法名(参数){ 方法体 }

不能拥有构造方法和静态代码块

8. 代理

8.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
interface Wedding{
void marry();
}
//被代理类
class Couple implements Wedding{
@Override
public void marry() {
System.out.println("give ring");
}
}
//代理类
class Proxy implements Wedding{

Wedding wedding;

public Proxy(Wedding wedding) {
this.wedding = wedding;
}

@Override
public void marry() {
System.out.println("prepare ring");
wedding.marry();
System.out.println("completed");
}
}

@Test
void testSystem() throws Exception {
Couple couple = new Couple();
Proxy proxy = new Proxy(couple);
/*prepare ring
give ring
completed
*/
proxy.marry();

}

8.2. 动态代理

public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces, InvocationHandler h)

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
interface Wedding{
void marry();
}

class Couple implements Wedding{
@Override
public void marry() {
System.out.println("give ring");
}
}

class dynamicProxy{

public static Object dynamic(Object obj){
return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("prepare ring");

Object invoke = method.invoke(obj, args);

System.out.println("completed");

return invoke;
}
});
}
}


@Test
void testSystem() throws Exception {
Couple couple = new Couple();
Wedding wedding = (Wedding)dynamicProxy.dynamic(couple);
/*
prepare ring
give ring
completed
*/
wedding.marry();

}

9. Object

Object作为所有类的父类

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
//native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。
public native int hashCode(){}
//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户比较字符串的值是否相等。
public boolean equals(Object obj){}
/*
naitive方法,用于创建并返回当前对象的一份拷贝。
一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,
x.clone().getClass() == x.getClass() 为true。
Object本身没有实现Cloneable接口,
所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常。
*/
protected native Object clone() throws CloneNotSupportedException{}
//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方法。
public String toString(){}
//native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,故不允许子类重写。
public final native Class<?> getClass(){}
//实例被垃圾回收器回收的时候触发的操作
protected void finalize() throws Throwable {}
/*
native方法,并且不能重写。
唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。
如果有多个线程在等待只会任意唤醒一个。
*/
public final native void notify(){}
/*
native方法,并且不能重写。
跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
*/
public final native void notifyAll(){}
/*
native方法,并且不能重写。
暂停线程的执行。
注意:sleep方法没有释放锁,而wait方法释放了锁 。
timeout是等待时间。
*/
public final native void wait(long timeout) throws InterruptedException{}
/*
多了nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。
所以超时的时间还需要加上nanos毫秒。
*/
public final void wait(long timeout, int nanos) throws InterruptedException{}
//跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
public final void wait() throws InterruptedException{}

9.1. ==

  • 对于基本数据类型,比较的是值
  • 对于引用数据类型,比较的是对象的内存地址

java中只有值传递,所以对于==来说,本质都是值的比较,只不过比较不同的数值还是对象地址值。

9.2. equals()

用于检测一个对象是否等于另一个对象

object类中的equals方法直接使用==运算符比较两个对象是否相等,是否具有相同的引用

1
2
3
public boolean equals(Object obj) {
return (this == obj);
}

子类可以对其进行重写。比较对象所属类,对象状态等

1
2
3
4
5
6
public boolean equals(Object obj) {
if (obj instanceof Byte) {
return value == ((Byte)obj).byteValue();
}
return false;
}

比较方法

1
2
3
4
5
6
7
Arrays.equals(type[] a,type[] b);
/*
如果a,b都为null,返回true
其中之一为null,返回false
否则返回a.equals(b)
*/
Objects.equals(Object a,Object b);

实例

1
2
3
4
5
6
7
8
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b为另一个引用,对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
System.out.println(aa == bb);// true
System.out.println(a == b);// false
System.out.println(a.equals(b));// true
System.out.println(42 == 42.0);// true

String 中的 equals 方法是被重写过的,当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。

String的equal方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}

9.3. hashCode()

1
public native int hashCode();

Object中hashCode方法返回对象在内存中地址转换成的一个int值,如果没有重写hashCode方法,任何对象的hashCode方法返回值都不相同。

在HashMap中key值是否重复就是通过hashCode来判断

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

在String类中散列码由内容导出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private int hash; // Default to 0
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;

for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}

//---------------
String s = "OK";
String t = new String("OK");
System.out.println(s.hashCode() +" \n"+t.hashCode()); //2524 2524

注意

规定equals被覆盖,hashCode必被覆盖

equals相同,hashCode必相同;equals不同,hashCode可能相同

9.4. hashCode() vs equals()

Java hashCode() 和 equals()的若干问题解答

9.5. toString()

1
2
3
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

默认返回 类名@xxxxx 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。

一般类都进行了重写,通过字符串串联内容,如ArrayList,使用StringBuilder串联数据,并且java编译自动调用toString方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public String toString() {
Iterator<E> it = iterator();
if (! it.hasNext())
return "[]";

StringBuilder sb = new StringBuilder();
sb.append('[');
for (;;) {
E e = it.next();
sb.append(e == this ? "(this Collection)" : e);
if (! it.hasNext())
return sb.append(']').toString();
sb.append(',').append(' ');
}
}

9.6. clone()

1
protected native Object clone() throws CloneNotSupportedException;

clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。子类只能调用受保护的clone方法克隆自己。重新定义clone,声明为public,才能让所有方法克隆对象。

类implements Cloneable,Cloneable接口作为克隆标识,类需要进行克隆处理。

1
2
3
4
5
6
public class Test implements Cloneable {
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}

###浅拷贝

拷贝对象和原始对象的引用类型引用同一个对象。浅复制仅仅复制所考虑的对象,不复制它所引用的对象

9.6.1. 深拷贝

拷贝对象和原始对象的引用类型引用两个相同的对象。深复制把复制的对象所引用的对象都复制了一遍

9.6.2. clone()的使用

Java中只有值传递,基本数据类型传的是值,对象传的是地址。

而clone()方法创建出一个与对象A具有相同状态的对象B,并且对B的修改不影响A的情况

所有类都继承自object类,object 类中有clone()方法,返回一个object对象的复制对象

clone()的使用:

class XX implements Cloneable + 重写clone()方法 + 调用super.clone()

其实现的是浅拷贝,当类中包括其他对象时,需要再调用clone()完成深复制

10. 内部类

内部类时定义在另一个类中的类。

  • 内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据
  • 内部类可以对同一个包中的其他类隐藏起来

内部类的对象总有一个隐式引用,指向创建它的外部类对象,内部类引用外部类的变量OuterClass.this.variable,生成内部类的实例OutObject.new InnerClass()

内部类声明的静态域必须是final,静态域意味着只有一份数据,而对于每个外部对象,会分别有一个单独的内部类实例。静态域不是final,则不是唯一的。

内部类不能有static方法。

拥有内部类的类编译后,内部类会被编译为OutClass$InnerClass.class

10.1. 静态内部类

内部类声明为static

不依赖于类实例化调用,不能有非静态变量和方法

1
2
3
4
5
public class TalkClock{
public static class TimePrinter implements ActionListener{

}
}

10.2. 成员内部类

依赖于实例化调用,不能有静态变量和方法

调用外部类成员变量 OutClass.this.XXX

1
2
3
4
5
6
7
8
9
public class TalkClock{
private boolean beep;

public class TimePrinter implements ActionListener{
boolean flag=TalkClock.this.beep;
}
}

TalkClock.TimePrinter tp=new TalkClock().new TimPrinter();

10.3. 局部内部类

方法中创建局部类

不能被public,protected,private以及static修饰

局部变量必须声明为final,jdk1.8+开始,final关键字可以省略

new出来的对象在堆内存中

局部变量跟方法走,在栈内存中

方法运行结束后,立刻出栈,局部变量立刻消失

new出来的对象在堆内存中持续存在,直到垃圾回收消失

TimePrinter类对象在堆内存中,而start方法生命周期在栈中,随出栈而消亡,为了TimePrinter类对象能继续使用该变量,传给类的是变量的副本,只能用不能改

1
2
3
4
5
6
7
8
public void start(){
final boolean beep = true;
class TimePrinter implements ActionListener{
if(beep){}
}

ActionListener listener=new TimePrinter();
}

10.4. 匿名内部类

没有类名的内部类,不使用extends,class,implements,没有构造函数,通过new 类名直接使用

1
2
3
4
5
ActionListener listener = new ActionListener(){
public void actionPerformed(ActionEvent event){
//...
}
};

11. 关键字

11.1. final

修饰变量:基本数据类型,不可重新赋值,必须被程序员显式初始化

修饰对象:指向对象地址不变,对象内容可变

修饰父类方法:不能被重写

修饰父类:不能被继承

11.2. static

作用于 说明
成员方法 静态方法可以通过类.静态方法调用
静态方法中不能使用非静态变量,方法。
代码块 独立于成员变量和成员函数,
类加载时会先执行static代码块,再初始化构造函数
内部类 不依赖于外部类实例对象而被实例化,
静态内部类不能与外部类有相同的名字,
不能访问外部类的普通成员变量和方法,只能访问其静态变量和方法
成员变量 静态变量从属于类,
在内存中只有一份,所有实例都指向同一个内存地址,
类被加载后,静态变量就会被分配空间,可以被使用,
引用该变量:类.静态变量,对象.静态变量
不能在成员函数内部定义static变量

拷贝对象和原始对象的引用类型引用不同对象。

拷贝对象和原始对象的引用类型引用同一个对象。

静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。最后才是构造函数的初始化。

初始化顺序:

  • 父类(静态变量、静态语句块)
  • 子类(静态变量、静态语句块)
  • 父类(实例变量、普通语句块)
  • 父类(构造函数)
  • 子类(实例变量、普通语句块)
  • 子类(构造函数)

11.3. switch

switch(expression),expression只能是一个枚举常量或整数表达式,expression可以是byte,short,char,int,也可以是它们的包装类Byte,Short,Character,Integer。long, float, double 强制转换为int可以使用。case为直接的常量数组,还可以为final型的变量,但不能是变量或带有变量的表达式。

Java7开始支持String类型,其实质是:对String调用hasCode()方法,得到hash值,来匹配所有case,接着调用字符串的equals()进行匹配。

11.4. volatile

volatile是一种类型修饰符,用来修饰被不同线程访问和修改的变量,保证变量是直接从内存中提取,而不利用缓存,从而保证变量值的一致性。

volatile不能保证操作的原子性,不能代替synchronized。

11.5. instanceof

判断一个引用类型的变量所指向的对象属于是一个类的实例,左边是否是右边的实例。

前面对象是后面类的实例或子类实例时都将返回true

11.6. stricfp

static float point精确浮点,严格按照IEEE二进制浮点数算术标准来执行产生理想的结果。用于修饰类,类中所有方法都会自动被stricfp修饰,保证浮点数运算精确性,在不同硬件平台上也有一致的结果,可能产生溢出。

java虚拟机默认允许将中间结果采用扩展的精度,不会产生溢出。

11.7. assert

作为一种软件调试的方法,主要作用是对一个boolean表达式进行检查。

assert expression或assert expression1:变量

如:assert 1+1==3:“assert failed,exit”,调用java -ea Test ,-ea启用 -da禁用

输出:Exception in thread “main” Java.lang.AssertError:assert failed, exit at Test.main(Test.java 5)

抛出AssertError异常

12. 泛型

泛型的本质是“数据类型的参数化”,处理的数据类型不是固定的,而是可以作为参数传入。可以把“泛型”理解为数据类型的一个占位符(类似于形式参数),即告诉编译器,在调用泛型时必须传入实际类型。这种参数类型可以用在类,接口和方法中,分别被称为泛型类,泛型接口,泛型方法。

  • 代码可以被不同类型的对象所重用。

  • 提供了编译期的类型安全,确保你只能把正确类型的对象放入集合中,避免了在运行时出现ClassCastException。

  • 泛型可以有多个参数<E1,E2,E3>

  • 泛型类的构造器不可以是public GenericClass<>()

  • 实例化后,操作原来泛型位置的结构必须与指定的泛型类型一致

  • 泛型不同的引用不能相互赋值 ArrayList<String>,ArrayList<Integer>不同

  • 泛型如果不指定,泛型对应的类型按照Object处理,但不等价于Object

  • 泛型不能使用基本数据结构

  • 在类/接口上声明的泛型,在本类或本接口中即代表某种类型,可以作为非静态属性的类型,非静态方法的参数类型,非静态方法的返回值类型。静态方法中不能使用类的泛型,但可以有静态泛型方法

    泛型方法所属的类与泛型类无关

    泛型参数是在调用方法时确定的,并非在实例化类时确定,故泛型方法可以是静态的

  • 异常类不能是泛型

  • 不能使用new E[],但是可以E[] elements = (E[]) new Object[capacity];

    泛型用于检查元素类型,在编译期会进行类型擦除,创建泛型数组而不知道数据类型,是不允许的

  • 父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型,子类必须是“富二代”,除了指定或保留父类的泛型,还可以增加自己的泛型

    子类不保留父类的泛型,按需实现:

    Class Father<T1,T2>{}

    • 没有类型,擦除 Class Son<A,B> extends Father{}
    • 具体类型 class Son<A,B> extends Father<Integer,String>{}

    子类保留父类的泛型,成为泛型子类

    • 全部保留 Class Son<T1,T2,A,B> extends Father<T1,T2>{}
    • 部分保留 Class Son<T2,A,B> extends Father<Integer,T2>{}

实际上,系统并没有创建带泛型的class文件,在编译时,不管泛型参数是什么,运行的是同样的类

1
2
3
ArrayList<String> list1 = new ArrayList<>();
ArrayList<Integer> list2 = new ArrayList<>();
System.out.println(list1.getClass()==list2.getClass()); //true

12.1. 定义泛型

泛型标记 对应单词 说明
E Element 在容器中使用,表示容器中的元素
T Type 表示普通的java类
K Key 表示键,如:Map中的键key
V Value 表示值
N Number 表示数值类型
表示不确定的java类型

12.1.1. 泛型类

public class 类名<泛型表示符合>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Generic<T>{

private T key;

public Generic(T key){
this.key=key;
}

public T getKey(){
return key;
}
}

//实例化
Generic<Integer> genericInteger=new Generic<Integer>(123);

12.1.2. 泛型接口

public interface 接口名<泛型表示符合>

1
2
3
4
public interface Genericator<T>{

public T method();
}

实现类可以指定类型,也可以不指定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Impl1<T> implements Generator<T>{

@Override
public T method(){
return null;
}
}

class Impl2 implements Generator<String>{

@Override
public String method(){
return "Hello World!";
}
}

12.1.3. 泛型方法

public <泛型类型> 泛型类型 方法名(泛型类型 行参)

静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。

public static <泛型> void 方法名(泛型 行参)

public static <泛型> 泛型 方法名(泛型 行参)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static <T> T getSum(T a){
return a;
}

//泛型方法
public <E> List<E> copy(E[] arr){
return Arrays.asList(arr);
}
@Test
void testSystem() {
Integer[] a = {1,2,3,4,5};
//泛型调用时,指明泛型参数的类型,泛型方法所属类与泛型类没有关系
List<Integer> in = copy(a);
//[1, 2, 3, 4, 5]
System.out.println(in);
}

12.2. 虚拟机中的泛型

  • 泛型是通过类型擦除来实现的,编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如List< String>在运行时仅用一个List来表示。无法在运行时访问到类型参数,因为编译器已经把泛型类型转换成了原始类型。

  • 限定类型变量:<T extends Comparable> T为绑定类型的子类型,绑定类型可以是类,也可以是接口,也可以是<T extends Comparable&Serializable>

  • 当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换

    1
    2
    3
    4
    Employee buddy = buddies.getFirst();
    //---------(1)原始方法的调用,(2)强制类型转换-----------
    Object obj = buddies.getFirst();
    Employee buddy = (Employee)obj;

12.3. 约束

12.3.1. 运行时类型查询只适用于原始类型

1
2
3
ArrayList<Integer> l1 = new ArrayList<>();
ArrayList<Double> l2 = new ArrayList<>();
System.out.println(l1.getClass()==l2.getClass()); //true

12.3.2. 不能创建参数化类型的数组

1
2
//Generic array creation
//ArrayList<Integer> list = new ArrayList<>[10];

因为擦除以后list的类型是ArrayList[],转换为Object[],数组会记住它的元素类型

1
2
3
4
5
6
7
8
//如果可行
//Object[] obj = list;

//ArrayStoreException异常,类型异常
//obj[0]="Hello";

//泛型存在擦除故能够通过数组存储检查,但导致类型错误
//obj[0]=new ArrayList<Double>();

12.3.3. 参数个数可变情况中

1
2
3
4
5
6
7
8
9
10
11
@SuppressWarning("unchecked")
public static <T> void addAll(Collection<T> coll,T... ts){
for(T t:ts){
coll.add(t);
}
}

Collection<Pair<String>> table = new Collection<>();
Pair<String> pair1,pair2;

addAll(pair1,pair2);

调用该方法,java虚拟机必须建立一个Pair< String> 数组

需标注警告 @SafeVarargs

12.3.4. 不能实例化类型变量

1
new T(); //error

12.3.5. 泛型类的静态上下文中类型变量无效

不能在静态域或方法中引用类型变量

因为类型擦除后,只包含一个singleInstance域

1
2
3
4
5
private static T singleInstance;//error
//error
private static T getSingleInstance(){

}

12.3.6. 不能抛出或捕获泛型类的实例

1
2
3
4
5
6
7
8
9
//error,泛型类不能扩展Throwable
public class Problem<T> extends Exception{ }

//catch子句中不能使用类型变量
try{

}catch(T e){//error

}

12.4. 通配符

泛型有两种限定通配符

  • 一种是<? extends T>它通过确保类型必须是T的子类来设定类型的上界
  • 另一种是<? super T>它通过确保类型必须是T的父类来设定类型的下界

泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。另一方面<?>表示了非限定通配符,因为<?>可以用任意类型来替代。

1
2
3
4
5
6
//返回值只能赋给一个Object
? getFirst();
//不能被调用,甚至不能用Object调用
void setFirst(?);
//可以用任意Object对象调用原始的Pair类方法
Pair<?>

12.5. 泛型数组

java允许创建无上限的通配符泛型数组

1
2
3
4
5
List<?>[] array = new ArrayList<>[10];
Object temp = array.get(0);
if(temp instanceOf String){
String s = (String) temp;
}

12.6. 泛型与反射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Class<?> c = Class.forName(name);
//泛型类变量数组
TypeVariable[] types = c.getTypeParameters();
//超类的泛型类
Type t = c.getGenericSuperClass();
//接口的泛型类
Type[] tp = c.getGenericInterfaces();

//泛型类的泛型方法
Method[] m = c.getDeclaredMethods();

//extends限定
Type[] c.getUpperBounds();
//super限定
Type[] c.getLowerBounds();

13. 异常Throwable

异常指程序运行时所发生的非正常情况或错误,Java把异常当作对象来处理。

异常基类为java.lang.Throwable ,子类:Error,Exception,Error是无法处理,应该尽量避免。

13.1. Error - unchecked

属于程序无法处理的错误 ,我们没办法通过 catch 来进行捕获

例如:系统内部错误和资源耗尽错误

StackOverFlowError溢出错误,OutOfMemoryError虚拟机内存不够等错误直接导致程序终止

必须解决该类问题

13.2. Exception

程序本身可以处理的异常,可以通过 catch 来进行捕获。Exception 又可以分为 受检查异常(必须处理) 和 不受检查异常(可以不处理)。

###IOException-checked

IOException,SQLException等发生不会导致程序出错,进行处理可以继续执行后续操作,编译器强制去捕获该类型的异常

子类方法中声明的已检查异常不能比超类方法声明的异常更通用

1
2
3
4
5
String readData() throws IOException{ }

if(!in.hasNext()){
throw new EOFException();
}

13.2.1. RuntimeException - unchecked

由JVM来处理,NullPointerException,ClassCastException,ArrayIndexOutOfBoundsException,ArrayStoreException,BufferOverflowException,ArithmeticException等,异常会一直往上层抛出,直到遇到处理代码为止。运行时异常,编译器不会强制对其进行捕获并处理。

出现异常:

  1. jvm根据异常产生的原因创建一个异常对象,这个异常对象包含了异常产生的内容,原因,位置;
  2. 查找try-catch,没有,jvm则把异常对象抛出给方法的调用者来处理异常
  3. 方法调用者收到异常对象,若没有异常处理逻辑则继续把异常对象抛出给上一层方法调用者,若始终没有处理者,最终由main的调用者jvm处理
  4. jvm接收到这个异常对象,把异常对象以红色字体打印到控制台
  5. jvm终止当前正在执行的java程序,中断程序

异常处理先捕获子类,再捕获父类异常

13.2.2. 创建异常类

extends Exception 编译期异常,必须try-catch或throws

extends RuntimeException 运行期异常,无需处理,交给jvm中断处理

1
2
3
4
5
6
7
8
9
10
11
12
13
class A extends Exception{
public A(){

}
public A(String meg){
super(meg);
}
}

String readData() throws A{
if(ch==-1)
throw A("文件结尾");
}

13.2.3. 捕获异常

不管有没有异常,finally内代码块都会执行

1
2
3
4
5
6
7
try{ 

}catch(Exception e){
e.getMessage();
}finally{

}

13.3. Throwable常用方法

1
2
3
4
5
6
7
8
9
10
11
12
//返回异常发生时的简要描述
public String getMessage(){}
//返回异常发生时的详细信息
public String toString(){}
/*
返回异常对象的本地化信息。
使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。
如果子类没有覆盖该方法,则该方法返回的信息与getMessage()返回的结果相同
*/
public String getLocalizedMessage(){}
//在控制台上打印 Throwable 对象封装的异常信息
public void printStackTrace(){}

13.4. throw,Objects,throws,try-catch,finally

13.4.1. throw

throw new xxxException();

使用throw关键字在指定的方法中抛出指定的异常

throw关键字必须写在方法的内部

throw关键字后new的对象必须是Exception类或其子类

throw关键字抛出指定的异常对象,必须处理,但RuntimeException和其子类对象可以不处理,默认交给jvm处理(打印异常,中断程序)

若throw后是编译异常,必须throws或try-catch

1
2
3
if(!in.hasNext()){
throw new EOFException();
}

13.4.2. Objects

public static<T> T requireNonNull(T obj) 查看指定引用对象是否为null

1
2
3
4
5
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}

public static boolean equals(Object a, Object b) 可用于判断两个对象内容相等

1
2
3
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}

13.4.3. throws

把异常抛出给方法的调用者处理,最终交给jvm处理

可以声明多个异常

调用一个声明抛出异常的方法,必须处理声明的异常

抛出的多个异常中存在父子关系,则只需要声明父类异常

子类的异常类型小于父类

方法名 throws AException, BException, ….{ }

1
2
3
4
public static void main(String[] args) throws FileNotFoundException {
File file = new File("");
OutputStream out = new FileOutputStream(file);
}

13.4.4. try-catch

try中出现的异常会抛出给catch处理

1
2
3
4
5
6
7
8
9
10
11
try{ 
//正常代码块
}catch(Exception e){
//常放入日志中
//throwable的简短描述
e.getMessage();
//throwable的详细消息字符串
e.toString();
//jvm打印异常对象的最全面消息
e.printStackTrace();
}

13.4.5. finally

和try一起使用,一般用于资源回收,不论是否异常都会执行

当try,finally中都存在return时,优先return的是finally中的

在以下 3 种特殊情况下,finally 块不会被执行:

  1. tryfinally块中用了 System.exit(int)退出程序。但是,如果 System.exit(int) 在异常语句之后,finally 还是会被执行
  2. 程序所在的线程死亡。
  3. 关闭 CPU。

13.5. try-with-resource

  1. 适用范围(资源的定义): 任何实现 java.lang.AutoCloseable或者 java.io.Closeable 的对象
  2. 关闭资源和 finally 块的执行顺序:try-with-resources 语句中,任何 catch 或 finally 块在声明的资源关闭后运行

《Effecitve Java》中明确指出:

面对必须要关闭的资源,我们总是应该优先使用 try-with-resources 而不是try-finally。随之产生的代码更简短,更清晰,产生的异常对我们也更有用。try-with-resources语句让我们更容易编写必须要关闭的资源的代码,若采用try-finally则几乎做不到这点。

13.5.1. IO流关闭资源

13.5.1.1. 使用try-catch-finally

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//读取文本文件的内容
Scanner scanner = null;
try {
scanner = new Scanner(new File("D://read.txt"));
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (scanner != null) {
scanner.close();
}
}

13.5.1.2. 使用try-with-resources

1
2
3
4
5
6
7
try (Scanner scanner = new Scanner(new File("test.txt"))) {
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException fnfe) {
fnfe.printStackTrace();
}

13.5.2. 多个资源关闭

try-with-resources块中使用分号分隔声明多个资源。

1
2
3
4
5
6
7
8
9
10
11
try (
BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
int b;
while ((b = bin.read()) != -1) {
bout.write(b);
}
}
catch (IOException e) {
e.printStackTrace();
}

13.6. 多异常捕获

try-catch-catch-… 上一级的catch必须是下一级catch的异常子类

13.7. 子父类异常

子类重写父类异常方法时,抛出的异常跟父类相同或是父类异常的子类

父类不抛出异常,子类重写该方法,子类产生异常只能捕获异常,不能抛出异常

子类背的锅不能比父类大

14. System

表示当前Java程序的运行平台。提供了一些类变量和类方法。

1
2
3
4
5
6
7
8
9
10
//当前项目路径
System.getProperty("user.dir");
//环境变量值
System.getenv("JAVA_HOME");
//时间 毫秒
System.currentTimeMillis();
//InputStream
System.in
//InputStream
System.out

15. 日志

15.1. 默认全局日志记录器

Logger.getGlobal()

1
2
3
4
5
6
7
8
9
import java.util.logging.Logger;

//九月 13, 2019 22:52:42 下午 com.runaccpeted.test main
//信息: File -> open
//自动包含时间,调用的类名和方法名
Logger.getGlobal().info("File -> open");

//取消所有的日志
Logger.getGlobal().setLevel(Level.OFF);

15.2.  自定义日志记录器

日志记录器级别:SEVERE,WARNING,INFO,CONFIG,FINE,FINER,FINEST

1
2
3
4
5
6
7
8
9
10
11
import java.util.logging.Logger;
-
private static final Logger logger = Logger.getLogger("记录器名");

//日志级别
logger.setLevel(Level.FINE);

//记录
logger.warning(message);
logger.fine(message);
logger.log(Level.FINE,message);
本文结束  感谢您的阅读
  • 本文作者: Wang Ting
  • 本文链接: /zh-CN/2019/09/13/Java基础知识/
  • 发布时间: 2019-09-13 21:03
  • 更新时间: 2021-12-31 15:55
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!