Java笔记-----(1)Java基础

Java笔记-----(1)Java基础

  • (1) 面向对象及其特性
    • (1.1)关于封装
    • (1.2)关于继承
      • 在 Java 中定义一个不做事且没有参数的构造方法的作用?
      • 覆盖(@Override),重写
      • 访问权限
      • super
    • (1.3)关于多态
      • 重载
    • (1.4)函数式编程与面向对象编程的比较
  • (2)JDK,JRE和JVM的区别与联系
      • Java的跨平台性是如何实现的呢?
  • (3)抽象类(abstract class)和接口(interface)的区别
    • 拓展:接口的并发量(限制接口流量)
  • (4)Java中数据类型
    • (4.1)基本数据类型
      • float 与 double
      • 隐式类型转换
    • (4.2)包装类型
    • (4.3)装箱与拆箱
      • 缓存池,Integer.valueOf() 方法
    • (4.4)自动装箱与自动拆箱
    • (4.5)基本类型与字符串之间的转换
  • (5)Java中的注解
    • (5.1)4 个元注解
    • (5.2)注解的相关说明
  • (6)Java中的反射机制
    • (6.1)相关解释
    • (6.2)反射的使用
  • (7)Java中的Exception和Error的区别
      • Throwable 类常用方法
      • 捕获异常应该遵循的原则
      • ClassNotFoundException与NoClassDefFoundError
      • StackOverflowError和OutOfMemoryError
  • (8)JIT编译器
    • (8.1)逃逸分析
  • (9)Java中的值传递和引用传递
    • 深拷贝 vs 浅拷贝
  • (10)String
    • (10.1)String为什么是不可变的?不可变有哪些好处?
    • (10.2)String,StringBuffer与StringBuilder的区别?
    • (10.3)字符串常量池 String Pool
    • (10.4)String s = new String("abc");
  • (11)Java中的泛型的理解
  • (12)Java序列化与反序列化的过程
      • 概述
      • 序列化和反序列代码实现
      • 序列化常见的面试题
  • (13)Object 通用方法
    • (13.1)概览
    • (13.2)Java中equals方法和==的区别? (重要)
    • (13.3)equals和hashCode方法的关系? (重要)
      • ① hashCode()介绍
      • ② 为什么要有 hashCode
      • ③ hashCode()与 equals()的相关规定
    • (13.4)Object.toString()
    • (13.5)Object.clone()
      • ① cloneable
      • ② 浅拷贝
      • ③ 深拷贝
      • ④ clone() 的替代方案
  • (14)Java和C++的区别有哪些?
  • (15)final 和 static 关键字
    • (15.1)关于 final 关键字的一些总结
    • (15.2)static关键字,静态与非静态的区别?
      • ① static 关键字的使用方法
      • ② 初始化顺序
      • ③ 静态和非静态的特点
      • ④ 在一个(静态方法)内调用一个(非静态成员)为什么是非法的?
      • ⑤ 静态方法和实例方法有何不同
  • (16)Java中的IO流
      • Java 中 IO 流分类
      • 既然有了字节流,为什么还要有字符流?
      • BIO、NIO 和 AIO
      • 同步和异步,阻塞和非阻塞

(1) 面向对象及其特性

面向对象是一种思想,可以将复杂问题简单化,让我们从执行者变为了指挥者。

  • 封装:将事物封装成一个类,减少耦合,隐藏细节。保留特定的接口与外界联系,当接口内部发生改变时,不会影响外部调用方。增加了程序的可读性。
  • 继承:从一个已知的类中派生出一个新的类,新类可以拥有已知类的行为和属性,并且可以通过覆盖/重写来增强已知类的能力。
  • 多态:多态的本质就是一个程序中存在多个同名的不同方法,主要通过三种方式来实现:
    1.通过子类对父类的覆盖来实现
    2.通过在一个类中对方法的重载来实现
    3.通过将子类对象作为父类对象使用来实现

(1.1)关于封装

封装主要是为了增加程序的可读性,解耦合并且隐藏部分实现细节。

public void setAge(int age) {
	if (age < 0 || age > 60)
	    throw new RuntimeException("年龄设置不合法");
	this.age = age;
}

通过将Student这个类的name和age属性私有化,只有通过公共的get/set方法才能进行访问,在get/set方法中我们可以对内部逻辑进行封装处理,外部的调用方不必关心我们的处理逻辑。

(1.2)关于继承

Java中不支持多继承,即一个类只可以有一个父类存在。另外Java中的构造函数是不可以继承的,如果构造函数被private修饰(单例模式),那么就是不明确的构造函数,该类是不可以被其它类继承的,具体原因我们可以先来看下Java中类的初始化顺序:

  • 初始化父类中的静态成员变量和静态代码块
  • 初始化子类中的静态成员变量和静态代码块
  • 初始化父类中的普通成员变量和代码块,再执行父类的构造方法 (注意这里是两步)
  • 初始化子类中的普通成员变量和代码块,再执行子类的构造方法

在 Java 中定义一个不做事且没有参数的构造方法的作用?

Java 程序在执行子类的构造方法之前,如果没有用 super()来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用 super()来调用父类中特定的构造方法,则编译时将发生错误,因为 Java 程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。

注意:构造方法没有返回值,但不能用 void 声明构造函数

如果父类构造函数是私有(private)的,则初始化子类的时候不可以被执行,所以解释了为什么该类不可以被继承,也就是说其不允许有子类存在。我们知道,子类是由其父类派生产生的,那么子类有哪些特点呢?

  • 子类拥有父类非private的属性和方法
  • 子类可以添加自己的方法和属性,即对父类进行扩展
  • 子类可以重新定义父类的方法,即方法的覆盖/重写

覆盖(@Override),重写

覆盖也叫重写,是指子类和父类之间方法的一种关系,比如说父类拥有方法A,子类扩展了方法A并且添加了丰富的功能。那么我们就说子类覆盖或者重写了方法A,也就是说子类中的方法与父类中继承的方法有完全相同的返回值类型、方法名、参数个数以及参数类型

访问权限

Java 中有三个访问权限修饰符:privateprotected 以及 public,如果不加访问修饰符,表示包级可见。
可以对类中的成员(字段以及方法)加上访问修饰符。

  • 类可见表示其它类可以用这个类创建实例对象。
  • 成员可见表示其它类可以用这个类的实例对象访问到该成员。

protected 用于修饰成员,表示在继承体系中成员对于子类可见,但是这个访问修饰符对于类没有意义。

super

  • 访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而委托父类完成一些初始化的工作
  • 访问父类的成员:如果子类重写了父类的某个方法,可以通过使用 super 关键字来引用父类的方法实现

(1.3)关于多态

通过方法的覆盖和重载可以实现多态。
多态主要通过三种方式来实现:

  • 通过子类对父类的覆盖来实现
  • 通过在一个类中对方法的重载来实现
  • 通过将子类对象作为父类对象使用来实现

重载

重载是指在一个类中(包括父类)存在多个同名的不同方法,这些方法的参数个数,顺序以及类型不同均可以构成方法的重载。如果仅仅是修饰符、返回值、抛出的异常不同,那么这是2个相同的方法。

public class OverLoadTest {
 
    public void method1(String name, int age){
        System.out.println("");
    }
    // 两个方法的参数顺序不同,可以构成方法的重载
    public void method1(int age, String name){
        System.out.println("");
    }
    //---------------------------------------------
    public void method2(String name){
        System.out.println("");
    }
    // 两个方法的参数类型不同,可以构成方法的重载
    public void method2(int age){
        System.out.println("");
    }
 
    //---------------------------------------------
    public void method3(String name){
        System.out.println("");
    }
    // 两个方法的参数个数不同,可以构成方法的重载
    public void method3(int age, int num){
        System.out.println("");
    }
}

只有方法返回值不同,可以构成重载吗?
调用某个方法,有时候并不关心其返回值,这个时候编译器根据方法名和参数无法确定我们调用的是哪个方法。

将子类对象作为父类对象使用来实现多态:
把不同的子类对象都当作父类对象来看,可以屏蔽不同子类对象之间的差异,写出通用的代码,做出通用的编程,以适应需求的不断变化。这样操作之后,父类的对象就可以根据当前赋值给它的子类对象的特性以不同的方式运作

对象的引用型变量具有多态性,因为一个引用型变量可以指向不同形式的对象,即:子类的对象作为父类的对象来使用。在这里涉及到了向上转型和向下转型,我们分别介绍如下:

向上转型:
子类对象转为父类,父类可以是接口。
公式:Father f = new Son(); Father是父类或接口,Son是子类。
向下转型:
父类对象转为子类。公式:Son s = (Son) f;

在向上转型的时候我们可以直接转,但是在向下转型的时候我们必须强制类型转换。并且,如案例中所述,该父类必须实际指向了一个子类对象才可强制类型向下转型,即其是以这种方式Father f = new Son()创建的父类对象。若以Father f = new Father()这种方式创建的父类对象,那么不可以转换向下转换为子类的Son对象,运行会报错,因为其本质还是一个Father对象。

(1.4)函数式编程与面向对象编程的比较

介绍:

  • 函数式编程,顾名思义,这种编程是以函数思维做为核心,在这种思维的角度去思考问题。这种编程最重要的基础是λ演算,接受函数当作输入和输出。
  • 面向对象编程,这种编程是把问题看作由对象的属性与对象所进行的行为组成。基于对象的概念,以类作为对象的模板,把类和继承作为构造机制,以对象为中心,来思考并解决问题。

优点

  • 函数式编程支持闭包和高阶函数,闭包是一种可以起函数的作用并可以如对象般操作的对象;而高阶函数是可以以另一个函数作为输入值来进行编程。支持惰性计算,这就可以在求值需要表达式的值得时候进行计算,而不是固定在变量时计算。还有就是可以用递归作为控制流程。函数式编程所编程出来的代码相对而言少很多,而且更加简洁明了。
  • 面向对象编程:面向对象有三个主要特征,分别是封装性、继承性和多态性。类的说明展现了封装性,类作为对象的模板,含有私有数据和公有数据,封装性能使数据更加安全依赖的就是类的特性,使得用户只能看到对象的外在特性,不能看到对象的内在属性,用户只能访问公有数据不能直接访问到私有数据。类的派生功能展现了继承性,继承性是子类共享父类的机制,但是由于封装性,继承性也只限于公有数据的继承(还有保护数据的继承),子类在继承的同时还可以进行派生。而多态性是指对象根据接收的信息作出的行为的多态,不同对象接收同一信息会形成多种行为。

缺点

  • 函数式编程:所有的数据都是不可以改变的,严重占据运行资源,导致运行速度也不够快。
  • 面向对象编程:为了编写可以重用的代码导致许多无用代码的产生,并且许多人为了面向对象而面向对象导致代码给后期维护带来很多麻烦。

(2)JDK,JRE和JVM的区别与联系

  • JDK(Java Development Kit)是一个开发工具包,是Java开发环境的核心组件,并且提供编译、调试和运行一个Java程序所需要的所有工具,可执行文件和二进制文件,是一个平台特定的软件
  • JRE(Java Runtime Environment)是指Java运行时环境,是JVM的实现,提供了运行Java程序的平台。JRE包含了JVM,但是不包含Java编译器/调试器之类的开发工具
  • JVM(Java Virtual Machine)是指Java虚拟机,当我们运行一个程序时,JVM负责将字节码转换为特定机器代码,JVM提供了内存管理/垃圾回收和安全机制等

区别与联系:

  • JDK是开发工具包,用来开发Java程序,而JRE是Java的运行时环境
  • JDK和JRE中都包含了JVM
  • JVM是Java编程的核心,独立于硬件和操作系统,具有平台无关性,而这也是Java程序可以一次编写,多处执行的原因

Java的跨平台性是如何实现的呢?

  • Java程序都是运行在Java虚拟机,即JVM之上。JVM屏蔽了底层操作系统和硬件的差异。
  • Java面向JVM编程,先编译生成字节码文件(.class),然后交给JVM解释成机器码执行 。先编译,再解释执行
  • Java的语言规范中规定了基本数据类型的取值范围和行为在各个平台上是保持一致的。

所以,Java是一种先编译,后解释执行的语言。

(3)抽象类(abstract class)和接口(interface)的区别

  • 抽象类中可以没有抽象方法,也可以抽象方法和非抽象方法共存
  • 接口中的方法在JDK8之前只能是抽象的,JDK8版本开始提供了接口中方法的default实现 ,接口的方法默认是public,其抽象方法为了被重写所以不能使用 private 关键字修饰!
  • 抽象类和类一样是单继承的;接口可以实现多个父接口(通过implements关键字)
  • 抽象类中可以存在普通的成员变量;接口中的变量必须是static final类型的,必须被初始化接口中只有常量,没有变量
  • 从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范

抽象类和接口应该如何选择?分别在什么情况下使用呢?

  • 仅仅需要定义一些抽象方法而不需要其余额外的具体方法或者变量,使用接口
  • 需要定义非抽象方法和变量,使用抽象类

JDK8接口中的方法默认实现:

public interface MyInterface {
    // 定义一个已经实现的方法,使用default表明
    default void say(String message){
        System.out.println("Hello "+message);
    }
    // 普通的抽象方法
    void test();
}

如果两个接口中存在同样的默认方法:

public interface MyInterface {
    // 定义一个已经实现的方法,使用default表明
    default void say(String message){
        System.out.println("Hello "+message);
    }
    // 普通的抽象方法
    void test();
}
interface MyInterface2{
    // 定义一个已经实现的方法,使用default表明
    default void say(String message){
        System.out.println("[2]-Hello "+message);
    }
}
// 此处会编译错误, 两个接口中都有say方法
class MyClass implements MyInterface, MyInterface2{
    @Override
    public void test() {
        System.out.println("test...");
    }
}

编译错误解决方案:

  • 重写多个接口中的相同的默认方法
class MyClass implements MyInterface, MyInterface2{
    @Override
    public void say(String message) {
        System.out.println("[Client]-Hello "+message);
    }
    @Override
    public void test() {
        System.out.println("test...");
    }
}
  • 在实现类中指定要使用哪个接口中的默认方法
class MyClass implements MyInterface, MyInterface2{
    // 手动指定哪个默认方法生效
    public void say(String message) {
        MyInterface.super.say(message);
    }
    @Override
    public void test() {
        System.out.println("test...");
    }
}

JDK8中为什么会出现默认方法呢?
使用接口,使得我们可以面向抽象编程,但是其有一个缺点就是当接口中有改动的时候,需要修改所有的实现类。在JDK8中,为了给已经存在的接口增加新的方法并且不影响已有的实现,所以引入了接口中的默认方法实现。

默认方法允许在不打破现有继承体系的基础上改进接口解决了接口的修改与现有的实现不兼容的问题。该特性在官方库中的应用是:给java.util.Collection接口添加新方法,如stream()、parallelStream()、forEach()和removeIf()等等。在我们实际开发中,接口的默认方法应该谨慎使用,因为在复杂的继承体系中,默认方法可能引起歧义和编译错误。

拓展:接口的并发量(限制接口流量)

参考文章:
JAVA中限制接口流量、并发的方法

JAVA中限制接口流量可以通过Guava的RateLimiter类或者JDK自带的Semaphore类来实现,两者有点类似,但是也有区别,要根据实际情况使用。简单来说,

  • RateLimiter类是控制以一定的速率访问接口。
  • Semaphore类是控制允许同时并发访问接口的数量

一、RateLimiter类
  RateLimiter翻译过来是速率限制器,使用的是一种叫令牌桶的算法,当线程拿到桶中的令牌时,才可以执行。通过设置每秒生成的令牌数来控制速率。使用例子如下:

public class TestRateLimiter implements Runnable {

    public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static final RateLimiter limiter = RateLimiter.create(1); // 允许每秒最多1个任务

    public static void main(String[] arg) {
        for (int i = 0; i < 10; i++) {
            limiter.acquire(); // 请求令牌,超过许可会被阻塞
            Thread t = new Thread(new TestRateLimiter());
            t.start();
        }
    }

    public void run() {
        System.out.println(sdf.format(new Date()) + " Task End..");
    }
}

二、Semaphore类
  Semaphore翻译过来是信号量,通过设置信号量总数,当线程拿到信号量,才可以执行,当执行完毕再释放信号量。从而控制接口的并发数量。使用例子如下:

初始化中的第二个参数true代表以公平的方式获取信号量,即先进先出的原则。

public class TestSemaphore implements Runnable {

    public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    
    public static final Semaphore semaphore = new Semaphore(5, true); // 允许并发的任务量限制为5个
    
    public static void main(String[] arg) {
        for (int i = 0; i < 10; i++) {
             Thread t = new Thread(new TestSemaphore());
             t.start();
        }
    }

    public void run() {
        try {
            semaphore.acquire(); // 获取信号量,不足会阻塞
            System.out.println(sdf.format(new Date()) + " Task Start..");
            Thread.sleep(5000);
            System.out.println(sdf.format(new Date()) + " Task End..");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release(); // 释放信号量
        }
    }
}

(4)Java中数据类型

(4.1)基本数据类型

Java笔记-----(1)Java基础_第1张图片

float 与 double

Java 不能隐式执行向下转型,因为这会使得精度降低。
1.1 字面量属于 double 类型,不能直接将 1.1 直接赋值给 float 变量,因为这是向下转型。

// float f = 1.1;

1.1f 字面量才是 float 类型。

float f = 1.1f;

隐式类型转换

因为字面量 1 是 int 类型,它比 short 类型精度要高,因此不能隐式地将 int 类型下转型为 short 类型。

short s1 = 1;
// s1 = s1 + 1;  不能隐式地将 int 类型下转型为 short 类型

但是使用 += 或者 ++ 运算符可以执行隐式类型转换。

s1 += 1;
// s1++;

上面的语句相当于将 s1 + 1 的计算结果进行了向下转型:

s1 = (short) (s1 + 1);

(4.2)包装类型

Java提供了两个类型系统,基本类型与引用类型,使用基本类型在于效率,然而很多情况,会创建对象使用,因为对象可以做更多的功能,如果想要我们的基本类型像对象一样操作,就可以使用基本类型对应的包装类,如下:

Java笔记-----(1)Java基础_第2张图片

(4.3)装箱与拆箱

基本类型与对应的包装类对象之间,来回转换的过程称为”装箱“与”拆箱“。

装箱:从基本类型转换为对应的包装类对象。

构造方法:

  • Integer(int value) 构造一个新分配的 Integer 对象,它表示指定的 int 值。
  • Integer(String s) 构造一个新分配的 Integer 对象,它表示 String 参数所指示的 int 值。传递的字符串,必须是基本类型的字符串,否则会抛出异常 "100" 正确 "a" 抛异常

静态方法:

  • static Integer valueOf(int i) 返回一个表示指定的 int 值的 Integer 实例。
  • static Integer valueOf(String s) 返回保存指定的 String 的值的 Integer 对象。

拆箱:从包装类对象转换为对应的基本类型。
成员方法:

  • int intValue() 以 int 类型返回该 Integer 的值。
//装箱:把基本类型的数据,包装到包装类中(基本类型的数据->包装类)
//构造方法
Integer in1 = new Integer(1);   // 方法上有横线,说明方法过时了
System.out.println(in1);//1 重写了toString方法

Integer in2 = new Integer("1");
System.out.println(in2);//1

//静态方法
Integer in3 = Integer.valueOf(1);
System.out.println(in3);

//Integer in4 = Integer.valueOf("a");//NumberFormatException数字格式化异常
Integer in4 = Integer.valueOf("1");
System.out.println(in4);

//拆箱:在包装类中取出基本类型的数据(包装类->基本类型的数据)
int i = in1.intValue();
System.out.println(i);

缓存池,Integer.valueOf() 方法

new Integer(123) 与 Integer.valueOf(123) 的区别在于:

  • new Integer(123) 每次都会新建一个对象;
  • Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。
Integer x = new Integer(123);
Integer y = new Integer(123);
System.out.println(x == y); // false

Integer z = Integer.valueOf(123);
Integer k = Integer.valueOf(123);
System.out.println(z == k); // true

valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。

public static Integer valueOf(int i) {
	if (i >= IntegerCache.low && i <= IntegerCache.high)
		return IntegerCache.cache[i + (-IntegerCache.low)];
	return new Integer(i);
}

在 Java 8 中,Integer 缓存池的大小默认为 -128~127。

编译器会在自动装箱过程调用 Integer.valueOf() 方法,因此多个值相同值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象

Integer m = 123;
Integer n = 123;
System.out.println(m == n); // true

(4.4)自动装箱与自动拆箱

由于我们经常要做基本类型与包装类之间的转换,从Java 5(JDK 1.5)开始,基本类型与包装类的装箱、拆箱动作可以自动完成。例如:

import java.util.ArrayList;

/*
    自动装箱与自动拆箱:基本类型的数据和包装类之间可以自动的相互转换
    JDK1.5之后出现的新特性
 */
public class Demo {
    public static void main(String[] args) {
        /*
            自动装箱:直接把int类型的整数赋值包装类
            Integer in = 1; 就相当于 Integer in = new Integer(1);
         */
        Integer in = 1;   //自动装箱
        
        /*
            自动拆箱:in是包装类,无法直接参与运算,可以自动转换为基本数据类型,在进行计算
            in+2;就相当于 in.intVale() + 2 = 3
            in = in.intVale() + 2 = 3 又是一个自动装箱
         */
        in = in+2;  // 拆箱
        
        ArrayList<Integer> list = new ArrayList<>();
        /*
            ArrayList集合无法直接存储整数,可以存储Integer包装类
            list是Integer类型列表,放入1有自动装箱过程。
         */
        list.add(1); //-->自动装箱 list.add(new Integer(1));
        int a = list.get(0); //-->自动拆箱  list.get(0).intValue();
    }
}

(4.5)基本类型与字符串之间的转换

基本类型转换为String:

//基本类型->字符串(String)

//方法一
int i1 = 100;
String s1 = i1+"";  // -> 字符串
System.out.println(s1+200);//100200

//方法二
String s2 = Integer.toString(100);
System.out.println(s2+200);//100200

//方法三
String s3 = String.valueOf(100);
System.out.println(s3+200);//100200

String转换成对应的基本类型:
除了Character类之外,其他所有包装类都具有parseXxx静态方法可以将字符串参数转换为对应的基本类型:

  • public static byte parseByte(String s):将字符串参数转换为对应的byte基本类型。
  • public static short parseShort(String s):将字符串参数转换为对应的short基本类型。
  • public static int parseInt(String s):将字符串参数转换为对应的int基本类型。
  • public static long parseLong(String s):将字符串参数转换为对应的long基本类型。
  • public static float parseFloat(String s):将字符串参数转换为对应的float基本类型。
  • public static double parseDouble(String s):将字符串参数转换为对应的double基本类型。
  • public static boolean parseBoolean(String s):将字符串参数转换为对应的boolean基本类型。

注意:如果字符串参数的内容无法正确转换为对应的基本类型,则会抛出java.lang.NumberFormatException异常。

//字符串(String)->基本类型
int i = Integer.parseInt(s1);
System.out.println(i-10);

不是数字类型的字符串,转成数字会报错。
int a = Integer.parseInt("a"); //NumberFormatException  不是数字类型的字符串
System.out.println(a);

(5)Java中的注解

Java 注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。注
解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。

(5.1)4 个元注解

Java中提供了4个元注解,元注解的作用是负责注解其它注解。分别是@Target,@Retention、@Documented、@Inherited

@Target
说明注解所修饰的对象范围,关键源码如下:

public @interface Target {  
    ElementType[] value();  
}  
public enum ElementType {  
  TYPE,FIELD,METHOD,PARAMETED,CONSTRUCTOR,LOCAL_VARIABLE,ANNOCATION_TYPE,PACKAGE,TYPE_PARAMETER,TYPE_USE  
}  

例如,如下的注解使用@Target标注,表明MyAnn注解就只能作用在类/接口方法成员变量上。

@Target({ElementType.TYPE, ElementType.METHOD,ElementType.FIELD})  
public @interface MyAnn {  
}

@Rentention:(保留策略)
保留策略定义了该注解被保留的时间长短。关键源码如下:

public @interface Retention {  
    RetentionPolicy value();  
}  
public enum RetentionPolicy {  
    SOURCE, CLASS, RUNTIME  
} 

其中,

  • SOURCE:表示在源文件中有效(即源文件保留)
  • CLASS:表示在class文件中有效(即class保留)
  • RUNTIME:表示在运行时有效(即运行时保留)

例如,@Retention(RetentionPolicy.RUNTIME)标注表示该注解在运行时有效。

@Documented
该注解用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被javadoc此类的工具文档化。Documented是一个标记注解,没有成员。关键源码如下:

public @interface Documented {
}

@Inherited
该注解是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的描述注解是否被子类继承。如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。关键源码如下:

public @interface Inherited {
}

(5.2)注解的相关说明

注解本质上就是一个接口,该接口默认继承Annotation接口

public interface MyAnno extends java.lang.annotation.Annotation {}  
通过javac编译, javap反编译

注解的作用:

  • 编写文档:通过代码里标识的注解生成文档【生成文档doc文档】
  • 代码分析:通过代码里标识的注解对代码进行分析【使用反射】
  • 编译检查:通过代码里标识的注解让编译器能够实现基本的编译检查【Override】
  • 代替繁杂的配置文件,简化开发

JDK中预定义的一些注解:

  • @Override :检测被该注解标注的方法是否是继承自父类(接口)的
  • @Deprecated:该注解标注的内容,表示已过时
  • @SuppressWarnings:压制警告,一般传递参数all @SuppressWarnings(“all”)

定义一个注解及其属性:

public @interface MyAnn {  
    String value();  
    int value1();  
}
// 使用注解MyAnn,可以设置属性
@MyAnn(value1=100,value="hello")  
public class MyClass {  
} 

定义了属性,在使用时需要给属性赋值:

  • 如果定义属性时,使用default关键字给属性默认初始化值,则使用注解时,可以不进行属性的赋值。
  • 如果只有一个属性需要赋值,并且属性的名称是value,则value可以省略,直接定义值即可。
  • 数组赋值时,值使用{}包裹。如果数组中只有一个值,则{}可以省略

当定义一个注解之后,还需要一个注解处理器来执行注解的内部逻辑。注解处理器定义了注解的处理逻辑,涉及到反射机制和线程机制等。

(6)Java中的反射机制

(6.1)相关解释

反射是框架设计的灵魂。

反射机制是指在运行中,对于任意一个类,都能够知道这个类的所有属性和方法。对于任意一个对象,都能够调用它的任意一个方法和属性。即动态获取信息动态调用对象方法的功能称为反射机制。

静态编译和动态编译:

  • 静态编译:在编译时确定类型,绑定对象
  • 动态编译:运行时确定类型,绑定对象

反射机制的作用:

  • 在运行时判断任意一个对象所属的类
  • 在运行时构造一个类的对象
  • 在运行时判断任意一个类所具有的成员变量和方法
  • 在运行时调用任意一个对象的方法,生成动态代理

反射机制的好处:

  1. 可以在程序运行过程中,操作这些对象。
  2. 可以解耦,提高程序的可扩展性。
  3. 可扩展性 :应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类。
  4. 类浏览器和可视化开发环境 :一个类浏览器需要可以枚举类的成员。可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码。
  5. 调试器和测试工具 : 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率。

缺点:

  • 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的 java 代码要慢很多。
  • 性能开销 :反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。
  • 安全限制 :使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。
  • 内部暴露 :由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。

(6.2)反射的使用

与反射相关的类:

  • Class:表示类,用于获取类的相关信息
  • Field:表示成员变量,用于获取实例变量和静态变量等
  • Method:表示方法,用于获取类中的方法参数和方法类型等
  • Constructor:表示构造器,用于获取构造器的相关参数和类型等

获取Class类有三种基本方式:
(1)通过类名称.class来获取Class类对象:

多用于参数的传递
Class c = int.class;
Class c = int[ ].class;
Class c = String.class

(2)通过对象.getClass( )方法来获取Class类对象:

多用于对象的获取字节码的方式
Class c = obj.getClass( );

(3)通过类名称加载类Class.forName( ),只要有类名称就可以得到Class:

多用于配置文件,将类名定义在配置文件中。读取文件,加载类
Class c = Class.forName(“cn.ywq.Demo”);

注意:同一个字节码文件(*.class)在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的Class对象都是同一个。


class 对象的功能:

* 获取功能:
	1. 获取成员变量们
		* Field[] getFields() :获取所有public修饰的成员变量
		* Field getField(String name)   获取指定名称的 public修饰的成员变量

		* Field[] getDeclaredFields()  获取所有的成员变量,不考虑修饰符
		* Field getDeclaredField(String name)  
	2. 获取构造方法们
		* Constructor<?>[] getConstructors()  
		* Constructor<T> getConstructor(<?>... parameterTypes)  

		* Constructor<T> getDeclaredConstructor(<?>... parameterTypes)  
		* Constructor<?>[] getDeclaredConstructors()  
	3. 获取成员方法们:
		* Method[] getMethods()  
		* Method getMethod(String name,<?>... parameterTypes)  

		* Method[] getDeclaredMethods()  
		* Method getDeclaredMethod(String name,<?>... parameterTypes)  

	4. 获取全类名	
		* String getName()  
* Field:成员变量
	* 操作:
		1. 设置值
			* void set(Object obj, Object value)  
		2. 获取值
			* get(Object obj) 
		3. 忽略访问权限修饰符的安全检查
			* setAccessible(true):暴力反射

* Constructor:构造方法
	* 创建对象:
		* T newInstance(Object... initargs)  
		* 如果使用【空参数构造方法】创建对象,操作可以简化:Class对象的newInstance方法

* Method:方法对象
	* 执行方法:
		* Object invoke(Object obj, Object... args)  
	* 获取方法名称:
		* String getName:获取方法名

以反射方式来创建对象的Demo:

public class Demo1 {
    public static void main(String[] args) throws Exception {
        String className = "com.ywq.User";
        // 获取Class对象
        Class clazz = Class.forName(className);
        // 创建User对象
        User user = (User)clazz.newInstance();
        // 和普通对象一样,可以设置属性值
        user.setUsername("yangwenqiang");
        user.setPassword("19931020");
 
        System.out.println(user);
    }
}
 
class User {
    private String username;
    private String password;
 
    public String getUsername() {
        return username;
    }
 
    public void setUsername(String username) {
        this.username = username;
    }
 
    public String getPassword() {
        return password;
    }
 
    public void setPassword(String password) {
        this.password = password;
    }
 
    @Override
    public String toString() {
        return "User [username=" + username + ", password=" + password + "]";
    }
}

(7)Java中的Exception和Error的区别

Java异常类层次结构图:
Java笔记-----(1)Java基础_第3张图片

  • Exception是程序正常运行中预料到可能会出现的错误,并且应该被捕获并进行相应的处理,是一种异常现象
  • Error是正常情况下不可能发生的错误,程序无法处理。Error会导致JVM处于一种不可恢复的状态,不需要捕获处理,比如说OutOfMemoryError

Exception又分为了运行时异常和编译时异常:

  • 编译时异常(受检异常)表示当前调用的方法体内部抛出了一个异常,所以编译器检测到这段代码在运行时可能会出异常,所以要求我们必须对异常进行相应的处理,可以捕获异常或者抛给上层调用方
  • 运行时异常(非受检异常)表示在运行时出现的异常,常见的运行时异常包括:
    数字转换异常,NumberFormatException
    空指针异常,NullPointerException(要访问的变量没有引用任何对象时,抛出该异常);
    算术异常,ArithmeticException(算术运算异常,一个整数除以 0 时,抛出该异常);
    数组越界异常,ArrayIndexOutOfBoundsException (下标越界异常)

Throwable 类常用方法

  • public string getMessage():返回异常发生时的简要描述
  • public string toString():返回异常发生时的详细信息
  • public string getLocalizedMessage():返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同
  • public void printStackTrace():在控制台上打印 Throwable 对象封装的异常信息

捕获异常应该遵循的原则

前边说到了异常Exception应该被捕获,我们可以使用try – catch – finally 来处理异常,并且使得程序恢复正常。

  • 尽可能捕获比较详细的异常,而不是使用Exception一起捕获。
  • 当本模块不知道捕获之后该怎么处理异常时,可以将其抛给上层模块。上层模块拥有更多的业务逻辑,可以进行更好的处理。
  • 捕获异常后至少应该有日志记录,方便之后的排查。
  • 不要使用一个很大的try – catch包住整段代码,不利于问题的排查。

ClassNotFoundException与NoClassDefFoundError

  • ClassNotFoundException:使用例如Class.forName方法来动态的加载该类的时候,传入了一个类名,但是其并没有在类路径中被找到的时候,就会报ClassNotFoundException异常。出现这种情况,一般都是类名字传入有误导致的。
  • NoClassDefFoundError:如果JVM或者ClassLoader实例尝试加载(可以通过正常的方法调用,也可能是使用new来创建新的对象)类的时候却找不到类的定义。但是要查找的类在编译的时候是存在的,运行的时候却找不到了。这个时候就会导致NoClassDefFoundError。出现这种情况,一般是由于打包的时候漏掉了部分类或者Jar包被篡改已经损坏

StackOverflowError和OutOfMemoryError

  • java.lang.StackOverflowError:如果一个线程在计算时,由于递归太深,所需要用到栈大小 > 配置允许最大的栈大小,那么Java虚拟机将抛出StackOverflowError,即栈溢出错误
  • java.lang.OutOfMemoryError:如果一个线程可以动态地扩展本机方法栈,并且尝试本地方法栈扩展(没有大于配置允许最大的栈大小),但是内存不足可以提供, 或者如果不能提供足够的内存来为新线程创建初始的堆(如new Object)。JVM不能分配给对象的创建空间,并且GC也不能够回收足够的空间JVM空间溢出,创建对象速度高于GC回收速度。那么Java虚拟机将抛出OutOfMemoryError,即内存不足错误

(8)JIT编译器

全名叫Just In Time Compile 也就是即时编译器,把经常运行的代码作为"热点代码"编译成与本地平台相关的机器码,并进行各种层次的优化。JIT编译除了具有缓存的功能外,还会对代码做各种优化,包括逃逸分析、锁消除、 锁膨胀、方法内联、空值检查消除、类型检测消除以及公共子表达式消除等。

(8.1)逃逸分析

逃逸分析的基本行为就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。JIT编译器的优化包括同步省略标量替换

  • 同步省略: 也就是锁消除,当JIT编译器判断不会产生并发问题,那么会将同步synchronized去掉
  • 标量替换: 在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。好处是:对象可以不在堆内存进行分配,为栈上分配提供了良好的基础。

相关概念:

  • 标量(Scalar) 是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。
  • 聚合量(Aggregate) 是还可以分解的数据。Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。

逃逸分析技术的缺点:
技术不是特别成熟,分析的过程也很耗时,如果没有一个对象是不逃逸的,那么就得不偿失了。

JVM -X参数

  • -Xint
    在解释模式 (interpreted mode)下,-Xint标记会强制JVM执行所有的字节码,当然这会降低运行速度,通常低10倍或更多。
  • -Xcomp
    -Xcomp 参数与它(-Xint)正好相反,JVM在第一次使用时会把所有的字节码编译成本地代码,从而带来最大程度的优化。
    然而,很多应用在使用 -Xcomp也会有一些性能损失,当然这比使用-Xint损失的少,原因是-xcomp没有让JVM启用JIT编译器的全部功能。JIT编译器可以对是否需要编译做判断,如果所有代码都进行编译的话,对于一些只执行一次的代码就没有意义了
  • -Xmixed
    -Xmixed 是混合模式,将解释模式与编译模式进行混合使用,由jvm自己决定,这是 jvm默认的模式,也是推荐使用的模式。

(9)Java中的值传递和引用传递

  • 值传递,意味着传递了对象的一个副本,即使副本被改变,也不会影响源对象(基本数据类型)
  • 引用传递,意味着传递的并不是实际的对象,而是对象的引用。因此,外部对引用对象的改变会反映到所有的对象上。(StringBuffer)
public class Test {

    public static void main(String[] args) {
        Student s1 = new Student("小张");
        Student s2 = new Student("小李");
        Test.swap(s1, s2);
        System.out.println("s1:" + s1.getName());
        System.out.println("s2:" + s2.getName());
    }

    public static void swap(Student x, Student y) {
        Student temp = x;
        x = y;
        y = temp;
        System.out.println("x:" + x.getName());
        System.out.println("y:" + y.getName());
    }
}

控制台输出:
x:小李
y:小张
s1:小张
s2:小李

Java笔记-----(1)Java基础_第4张图片

Java笔记-----(1)Java基础_第5张图片
方法并没有改变存储在变量 s1 和 s2 中的对象引用。swap 方法的参数 x 和 y 被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝

Java 中方法参数的使用情况:

  • 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。
  • 一个方法可以改变一个对象参数的状态
  • 一个方法不能让对象参数引用一个新的对象。

深拷贝 vs 浅拷贝

  • 浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
  • 深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。

(10)String

(10.1)String为什么是不可变的?不可变有哪些好处?

String 被声明为 final,因此它不可被继承。
在 Java 8 中,String 内部使用 char 数组存储数据。

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
	/** The value is used for character storage. */
	private final char value[];
}

在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 coder 来标识使用了哪种编码

public final class String
	implements java.io.Serializable, Comparable<String>, CharSequence {
	/** The value is used for character storage. */
	private final byte[] value;
	/** The identifier of the encoding used to encode the bytes in {@code value}. */
	private final byte coder;
}

value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value
数组的方法,因此可以保证 String 不可变

不可变的好处:

  1. 可以缓存 hash 值
    因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。
  2. String Pool 的需要
    如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用
    String Pool。
  3. 安全性
    String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变
    的,那么在网络连接过程中,String 被改变,改变 String 对象的那一方以为现在连接的是其它主机,而实际情况却
    不一定是。
  4. 线程安全
    String 不可变性天生具备线程安全,可以在多个线程中安全地使用。

(10.2)String,StringBuffer与StringBuilder的区别?

① 可变性
String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[]
在 Java 9 之后,String 类的实现改用 byte 数组存储字符串, private final byte[] value

而 StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的。

② 线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。

AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。

  • StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁synchronized,所以是线程安全的
  • StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。

StringBuffer 线程安全 效率低
记忆:fe safe

③ 性能

  • 每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。
  • StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。
  • 相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。

④ 总结

  • 操作少量的数据: 适用 String
  • 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
  • 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer

(10.3)字符串常量池 String Pool

字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。不仅如
此,还可以使用 String 的 intern() 方法在运行过程中将字符串添加到 String Pool 中。

当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法
进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回
这个新字符串的引用

下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同字符串,而 s3 和 s4 是通过 s1.intern() 方法取得一
个字符串引用。intern() 首先把 s1 引用的字符串放到 String Pool 中,然后返回这个字符串引用。因此 s3 和 s4 引用
的是同一个字符串。

String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s1 == s2); // false

String s3 = s1.intern();
String s4 = s1.intern();
System.out.println(s3 == s4); // true

如果是采用 “bbb” 这种字面量的形式创建字符串,会自动地将字符串放入 String Pool 中。

String s5 = "bbb";
String s6 = "bbb";
System.out.println(s5 == s6); // true

在 Java 7 之前,String Pool 被放在运行时常量池中,它属于永久代。而在 Java 7,String Pool 被移到中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。

(10.4)String s = new String(“abc”);

使用这种方式一共会创建两个字符串对象(前提是 String Pool 中还没有 “abc” 字符串对象)。

  • “abc” 属于字符串字面量,因此编译时期会在 String Pool 中创建一个字符串对象,指向这个 “abc” 字符串字面量;
  • 而使用 new 的方式会在堆中创建一个字符串对象。

(11)Java中的泛型的理解

使用泛型的好处:

  • 使用泛型能写出更加灵活通用的代码
  • 泛型将代码安全性检查提前到编译期
  • 泛型能够省去类型强制转换
public class Box<T> {
	// T stands for "Type"
	private T t;
	public void set(T t) { this.t = t; }
	public T get() { return t; }
}

拓展阅读:10 道 Java 泛型面试题

(12)Java序列化与反序列化的过程

参考文章:
Java笔试面试-克隆和序列化
Java序列化与反序列化

概述

内存中的数据对象只有转换成二进制流才能进行数据持久化或者网络传输,将对象转换成二进制流的过程叫做序列化(Serialization);相反,把二进制流恢复为数据对象的过程就称之为反序列化(Deserialization)。

为什么要用序列化与反序列化
对象序列化的两种用途:

  • 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
  • 在网络上传送对象的字节序列。

Java 序列化中如果有些字段不想进行序列化,怎么办?
对于不想进行序列化的变量,使用 transient 关键字修饰。

transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和方法。

什么是序列化,如何实现序列化?
Java 中对象的序列化就是将对象转换成二进制序列,反序列化则是将二进制序列转换成对象。
采用Java序列化与反序列化技术:

  • 一是可以实现数据的持久化,在MVC模式中很是有用;
  • 二是可以对象数据的远程通信。

Java 实现序列化的多种方式

  • 首先需要使用到工具类 ObjectInputStream 和ObjectOutputStream 两个IO类
  • 实现 Serializable 接口
  • 实现 Externalizable 接口

序列化和反序列代码实现

先把对象序列化到磁盘,再从磁盘中反序列化出对象,请参考以下代码:

class SerializableTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // 对象赋值
        User user = new User();
        user.setName("大冰");
        user.setAge(20);
        System.out.println(user);
        // 创建输出流(序列化内容到磁盘)
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.out"));
        // 序列化对象
        oos.writeObject(user);
        oos.flush();
        oos.close();
        // 创建输入流(从磁盘反序列化)
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.out"));
        // 反序列化
        User user2 = (User) ois.readObject();
        ois.close();
        System.out.println(user2);
    }
}
class User implements Serializable {
    private static final long serialVersionUID = 3831264392873197003L;
    private String name;
    private int age;
    @Override
    public String toString() {
        return "{name:" + name + ",age:" + age + "}";
    }
    // setter/getter...
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}

程序执行结果:

{name:大冰,age:20}
{name:大冰,age:20}

序列化常见的面试题

1.serialVersionUID 的作用是什么?
答:如果显示定义了 serialVersionUID 值之后,可以使序列化和反序列化向后兼容。也就是说如果 serialVersionUID 的值相同,修改对象的字段(删除或增加),程序不会报错,之后给没有的字段赋值为 null,而如果没有指定 serialVersionUID 的值,如果修改对象的字段,程序就会报错。

2.可序列化接口(Serializalbe)的用途是什么?
答:可序列化 Serializalbe 接口存在于 java.io 包中,构成了 Java 序列化机制的核心,它没有任何方法,它的用途是标记某对象为可序列化对象,指示编译器使用 Java 序列化机制序列化此对象。

3.常用的序列化方式都有哪些?
Java 原生序列化方式
JSON 格式,可使用 fastjson 或 GSON
Hessian 方式序列化

(13)Object 通用方法

(13.1)概览

public native int hashCode()
public boolean equals(Object obj)
protected native Object clone() throws CloneNotSupportedException
public String toString()
public final native Class<?> getClass()
protected void finalize() throws Throwable {}
public final native void notify()
public final native void notifyAll()
public final native void wait(long timeout) throws InterruptedException
public final void wait(long timeout, int nanos) throws InterruptedException
public final void wait() throws InterruptedException

(13.2)Java中equals方法和==的区别? (重要)

对任何不是 null 的对象 x 调用 x.equals(null) 结果都为 false

  • 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。
  • 对于引用类型,== 判断两个变量是否引用同一个对象,而 equals() 判断引用的对象是否等价。

equals是判断两个变量或者实例指向同一个内存空间的值是不是相同
==是判断两个变量或者实例是不是指向同一个内存空间


==:它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)。
equals():它的作用也是判断两个对象是否相等。但它一般有两种使用情况:

  • 情况 1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
  • 情况 2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
public class test1 {
    public static void main(String[] args) {
        String a = new String("ab"); // a 为一个引用
        String b = new String("ab"); // b为另一个引用,对象的内容一样
        String aa = "ab"; // 放在常量池中
        String bb = "ab"; // 从常量池中查找
        if (aa == bb) // true
            System.out.println("aa==bb");
        if (a == b) // false,非同一对象
            System.out.println("a==b");
        if (a.equals(b)) // true
            System.out.println("aEQb");
        if (42 == 42.0) { // true -----------------this is true------------------
            System.out.println("true");
        }
    }
}

说明:

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

(13.3)equals和hashCode方法的关系? (重要)

Object.equals(): 用来判断两个对象是否相同,在Object类中是通过判断对象间的内存地址来决定是否相同
Object.hashCode(): 获取哈希码,也称为散列码,返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。

  • hashCode主要用于提升查询效率提高哈希表性能,来确定在散列结构中对象的存储地址
  • 重写equals()必须重写hashCode()
  • 哈希存储结构中,添加元素重复性校验的标准就是先检查hashCode值,后判断equals()
    两个对象equals()相等,hashcode()必定相等
    两个对象hashcode()不等,equals()必定也不等
    两个对象hashcode()相等,对象不一定相等,需要通过equals()进一步判断

面试官:你重写过 hashcode 和 equals 么,为什么重写 equals 时必须重写 hashCode 方法

① hashCode()介绍

hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置

hashCode() 定义在 JDK 的 Object.java 中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码(可以快速找到所需要的对象)

② 为什么要有 hashCode

我们先以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode: 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与该位置其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals() 方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。

通过我们可以看出:hashCode() 的作用就是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()在散列表中才有用,在其它情况下没用。在散列表中 hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。

③ hashCode()与 equals()的相关规定

  • 如果两个对象相等,则 hashcode 一定也是相同的,对两个对象分别调用 equals 方法都返回 true
  • 两个对象有相同的 hashcode 值,它们也不一定是相等的
  • 因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖
  • hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。即重写 equals 时必须重写 hashCode 方法

(13.4)Object.toString()

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

(13.5)Object.clone()

① cloneable

clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类
实例的 clone() 方法

public class CloneExample {
	private int a;
	private int b;
}

CloneExample e1 = new CloneExample();
// CloneExample e2 = e1.clone(); // 'clone()' has protected access in 'java.lang.Object'

重写 clone() 得到以下实现:

public class CloneExample {
	private int a;
	private int b;
	
	@Override
	public CloneExample clone() throws CloneNotSupportedException {
		return (CloneExample)super.clone();
	}
}

CloneExample e1 = new CloneExample();
try {
	CloneExample e2 = e1.clone();
} catch (CloneNotSupportedException e) {
	e.printStackTrace();
}

java.lang.CloneNotSupportedException: CloneExample

以上抛出了 CloneNotSupportedException,这是因为 CloneExample 没有实现 Cloneable 接口。
应该注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只
是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException

public class CloneExample implements Cloneable {
	private int a;
	private int b;
	
	@Override
	public Object clone() throws CloneNotSupportedException {
		return super.clone();
	}
}

② 浅拷贝

拷贝对象和原始对象的引用类型引用同一个对象
浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。

public class ShallowCloneExample implements Cloneable {
	private int[] arr;
	
	public ShallowCloneExample() {
		arr = new int[10];
		for (int i = 0; i < arr.length; i++) {
			arr[i] = i;
		}
	}
	
	public void set(int index, int value) {
		arr[index] = value;
	}
	
	public int get(int index) {
		return arr[index];
	}
	
	@Override
	protected ShallowCloneExample clone() throws CloneNotSupportedException {
		return (ShallowCloneExample) super.clone();
	}
}

ShallowCloneExample e1 = new ShallowCloneExample();
ShallowCloneExample e2 = null;
try {
	e2 = e1.clone();
} catch (CloneNotSupportedException e) {
	e.printStackTrace();
}

//意思是e1和e2指向同一个对象
e1.set(2, 222);
System.out.println(e2.get(2)); // 222

③ 深拷贝

拷贝对象和原始对象的引用类型引用不同对象
深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。

public class DeepCloneExample implements Cloneable {
	private int[] arr;
	
	public DeepCloneExample() {
		arr = new int[10];
		for (int i = 0; i < arr.length; i++) {
			arr[i] = i;
		}
	}
	
	public void set(int index, int value) {
		arr[index] = value;
	}
	
	public int get(int index) {
		return arr[index];
	}
	
	@Override
	protected DeepCloneExample clone() throws CloneNotSupportedException {
		DeepCloneExample result = (DeepCloneExample) super.clone();

		//创建一个新的对象,并复制其内容
		result.arr = new int[arr.length];
		for (int i = 0; i < arr.length; i++) {
			result.arr[i] = arr[i];
		}
		return result;
	}
}

DeepCloneExample e1 = new DeepCloneExample();
DeepCloneExample e2 = null;
try {
	e2 = e1.clone();
} catch (CloneNotSupportedException e) {
	e.printStackTrace();
}
//意思是e1和e2指向不同的对象
e1.set(2, 222);
System.out.println(e2.get(2)); // 2

④ clone() 的替代方案

使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,
最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。

public class CloneConstructorExample {
	private int[] arr;
	
	public CloneConstructorExample() {
		arr = new int[10];
		for (int i = 0; i < arr.length; i++) {
			arr[i] = i;
		}
	}
	
	//拷贝构造函数
	public CloneConstructorExample(CloneConstructorExample original) {
		arr = new int[original.arr.length];
		for (int i = 0; i < original.arr.length; i++) {
			arr[i] = original.arr[i];
		}
	}
	
	public void set(int index, int value) {
		arr[index] = value;
	}
	
	public int get(int index) {
		return arr[index];
	}
}

CloneConstructorExample e1 = new CloneConstructorExample();
CloneConstructorExample e2 = new CloneConstructorExample(e1);
e1.set(2, 222);
System.out.println(e2.get(2)); // 2,深拷贝

(14)Java和C++的区别有哪些?

  • 都是面向对象的语言,都支持封装、继承和多态
  • Java 不提供指针来直接访问内存,程序内存更加安全
  • Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承
  • Java 有自动内存管理机制,不需要程序员手动释放无用内存
  • 在 C 语言中,字符串或字符数组最后都会有一个额外的字符‘\0’来表示结束。但是,Java 语言中没有结束符这一概念。

  • Java 是纯粹的面向对象语言,所有的对象都继承自 java.lang.Object,C++ 为了兼容 C 即支持面向对象也支持
    面向过程。
  • Java 通过虚拟机从而实现跨平台特性,但是 C++ 依赖于特定的平台。
  • Java 没有指针,它的引用可以理解为安全指针,而 C++ 具有和 C 一样的指针。
  • Java 支持自动垃圾回收,而 C++ 需要手动回收。
  • Java 不支持多重继承,只能通过实现多个接口来达到相同目的,而 C++ 支持多重继承。
  • Java 不支持操作符重载,虽然可以对两个 String 对象执行加法运算,但是这是语言内置支持的操作,不属于操作符重载,而 C++ 可以。
  • Java 的 goto 是保留字,但是不可用,C++ 可以使用 goto。
  • Java 不支持条件编译,C++ 通过 #ifdef #ifndef 等预处理命令从而实现条件编译。

(15)final 和 static 关键字

(15.1)关于 final 关键字的一些总结

final 关键字主要用在三个地方:变量、方法、类。

  1. 数据
    声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。
    对于一个 final 变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象,但是被引用的对象本身是可以修改的


  2. 当用 final 修饰一个类时,表明这个类不能被继承。final 类中的所有成员方法都会被隐式地指定为 final 方法

  3. 方法
    使用 final 方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义,声明方法不能被子类重写;第二个原因是效率
    在早期的 Java 实现版本中,会将 final 方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的 Java 版本已经不需要使用 final 方法进行这些优化了)。
    类中所有的 private 方法都隐式地指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是重写基类方法,而是在子类中定义了一个新的方法

(15.2)static关键字,静态与非静态的区别?

静态是指被static修饰符修饰的,包括类、方法、变量、块等。

① static 关键字的使用方法

1 静态变量

  • 静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它。静态变量在内存中只存在一份
  • 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。

2 静态方法
静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法。

public abstract class A {
	public static void func1(){
	}
	// public abstract static void func2(); 
	// Illegal combination of modifiers: 'abstract' and 'static'
}

只能访问所属类的静态字段和静态方法方法中不能有 this 和 super 关键字

public class A {
	private static int x;
	private int y;
	
	public static void func1(){
		int a = x;
		// int b = y; // Non-static field 'y' cannot be referenced from a static context
		// int b = this.y; // 'A.this' cannot be referenced from a static context
	}
}

3 静态语句块
静态语句块在类初始化时运行一次。

public class A {
	static {
		System.out.println("123");
	}
	
	public static void main(String[] args) {
		A a1 = new A();
		A a2 = new A();
	}
}
123

4 静态内部类
非静态内部类依赖于外部类的实例,而静态内部类不需要。

public class OuterClass {
	//非静态内部类依赖于外部类的实例
	class InnerClass { 
	}
	
	static class StaticInnerClass {
	}
	
	public static void main(String[] args) {
		// InnerClass innerClass = new InnerClass(); 
		// 'OuterClass.this' cannot be referenced from a static context
		OuterClass outerClass = new OuterClass();
		InnerClass innerClass = outerClass.new InnerClass();
		StaticInnerClass staticInnerClass = new StaticInnerClass();
	}
}

静态内部类不能访问外部类的非静态的变量和方法

5 静态导包
在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。

import static com.xxx.ClassName.*

② 初始化顺序

静态变量和静态语句块优先于实例变量和普通语句块,静态变量静态语句块的初始化顺序取决于它们在代码中的顺
序。

public static String staticField = "静态变量";

static {
	System.out.println("静态语句块");
}

public String field = "实例变量";

{
	System.out.println("普通语句块");
}

最后才是构造函数的初始化。

public InitialOrderTest() {
	System.out.println("构造函数");
}

存在继承的情况下,初始化顺序为:

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

③ 静态和非静态的特点

静态特点:
1.一声明就被储存在中,直接占据内存,可以快速稳定的调用;
2.生命周期长,从JVM加载开始到JVM卸载结束
3.全局唯一:在一个运行环境中,静态变量只有一个值,任何一次修改都是全局性影响;
4.占据内存,程序中应包含尽量少的static;

非静态是指没被static修饰的,特点:
1.new的时候占据内存,实例化后调用;
2.非静态变量赋值不发生冲突;

④ 在一个(静态方法)内调用一个(非静态成员)为什么是非法的?

由于(静态方法)可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员

⑤ 静态方法和实例方法有何不同

  • 在外部调用静态方法时,可以使用"类名.方法名“的方式,也可以使用”对象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
  • 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制。

(16)Java中的IO流

Java 中 IO 流分类

  • 按照流的流向分,可以分为输入流输出流
  • 按照操作单元划分,可以划分为字节流字符流
  • 按照流的角色划分为节点流处理流

Java Io 流共涉及 40 多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。

  • InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流
  • OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流

按操作方式分类结构图:
Java笔记-----(1)Java基础_第6张图片
按操作对象分类结构图:
Java笔记-----(1)Java基础_第7张图片

既然有了字节流,为什么还要有字符流?

问题本质想问:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作字符流操作呢?

字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。

BIO、NIO 和 AIO

  • BIO (Blocking I/O): 同步阻塞 I/O 模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机 1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
    Java笔记-----(1)Java基础_第8张图片
  • NIO (New I/O): NIO 是一种同步非阻塞的 I/O 模型,在 Java 1.4 中引入了 NIO 框架,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它支持面向缓冲的,基于通道的 I/O 操作方法。

NIO 提供了与传统 BIO 模型中的 SocketServerSocket 相对应的 SocketChannelServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。

  • 阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;
  • 非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞 I/O (BIO)来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。

Java笔记-----(1)Java基础_第9张图片
一个线程可以管理多个连接,减少线程多的压力
Java笔记-----(1)Java基础_第10张图片

Java笔记-----(1)Java基础_第11张图片

  • AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO2,它是异步非阻塞的 IO 模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步 IO 的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO 操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。

Java笔记-----(1)Java基础_第12张图片

Java笔记-----(1)Java基础_第13张图片

同步和异步,阻塞和非阻塞

参考文章:
IO多路复用机制详解

同步和异步的概念描述的是用户线程与内核的交互方式

  • 同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;
  • 而异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数。

阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式

  • 阻塞是指IO操作需要彻底完成后才返回到用户空间;
  • 而非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成。

你可能感兴趣的:(Java笔记,java)