1_基础篇

一、常见基础

Java有哪些特点?

1、面向对象(封装、继承、多态)

2、跨平台性(Java虚拟机实现跨平台性)

3、可靠性(具备异常处理和自动内存管理机制)

4、安全性(Java 语言本身的设计就提供了多重安全防护机制如访问权限修饰符、限制程序直接访问操作系统资源)

5、高效性(通过 Just In Time 编译器等技术的优化,Java 语言的运行效率还是非常不错的)

6、支持网络编程并且很方便

什么是JRE、JDK、JVM?

JRE(Java Runtime Environment)是指Java运行时环境,如果要运行只是想要运行Java代码,那么只安装JRE就可以了。

JDK(Java Development Kit) 是指Java开发工具包,顾名思义,是用于Java开发者的工具包,JDK中包含了JRE,除了JRE之外还提供了java开发所需要的核心类库和很多的工具,例如javac、javap、javadoc等等。

JVM是指运行Java字节码的虚拟机。也是Java实现跨平台性的的关键所在,不同的操作系统(Windows、MAC、Linux)下载对应版本的JVM,运行同一套字节码文件,所产生的结果是一样的。

三者之间的关系如下图所示:

在这里插入图片描述

Java有哪些数据类型?

Java的数据类型可分为两大类,一种是基本数据类型,一种是引用数据类型

在Java中基本数据类型只有八种,其他的全是引用数据类型(枚举、类、数组等),基本数据类型如下表所示:

数据类型占用字节(byte)占用位数(bit)包装类数值长度
booleanBoolean
char216Character
byte18Byte-128~127(-2的7次方到2的7次方-1)
short216Short-32768~32767(-2的15次方到2的15次方-1)
int432Integer-2的31次方~2的31次方-1
long864Long-2的63次方~2的63次方-1
float432Float
double864Double

注意:在Java中boolean占用的字节数会根据虚拟机的判断来进行分配,单个的boolean会被编译为int类型,所以占用4字节,boolean数组会被编译为byte数组,所以占用1字节,具体还要看虚拟机是否按照规范来,所以boolean占用1个字节或者4个字节都是有可能的。

int和Integer的区别

1、int是基本数据类型,Integer是int的包装类,属于引用数据类型

2、int的默认值是0,Integer的默认值是null

3、int存储的是数据的值,Integer存储的是数据的引用,存储的是数据在内存中的地址

将超出范围的数值赋值给一个整数类型会怎么样?比如将129赋值给一个byte类型

将129赋值给一个byte类型的结果为-127,在Java中负数是以补码的形式表示的。这里需要补充说一下原码、反码、补码。

原码:一个数的二进制表示为原码

反码:除符号位外其他的位取反,即1变0,0变1

补码:正数的补码与原码相同,负数的补码为反码+1

正数的原码、反码、补码都是一样的;负数的反码为符号位不变,其余位取反(即1变0,0变1),负数的补码为反码+1

所以当129赋值给一个byte类型时,129的原码为10000001,第一位为符号位(符号位0为整数,1为负数),1为负数,129的反码为其余位取反0000001变为1111110,然后129的补码为反码+1,1111110+1变为1111111,转换为十进制为127,再加上符号位为负数,所以最后结果为-127,其他的整数类型也是按照这个规则运算。

short s = 1; s = s + 1;正确吗?short s = 1; s += 1;正确吗?

前者错误:s + 1结果是int类型,赋值给short类型的变量会编译报错。后者正确:s += 1;相当于s = (short)(s + 1);其中有隐含的强制类型转换。

Java的常量池技术

Java中用静态cache数组存储-128到127的数据,Byte , Short,Integer,Long ,Character,Boolean。若Integer等类型在自动装箱等初始化操作时,值在-128~127之间就不用额外申请空间,这也叫做享元模式。

享元模式(Flyweight Pattern): 运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。

以下代码的输出

Integer i1 = 127;
int i2 = 127;
Integer i3 = Integer.valueOf(127);
System.out.println(i1 == i2); // true
System.out.println(i1 == i3); // true

int j1 = 128;
Integer j2 = 128;
Integer j3 = 128;
Integer j4 = Integer.valueOf(128);
Integer j5 = new Integer(128);
Integer j6 = new Integer(128);
System.out.println(j1 == j2); // true
System.out.println(j2 == j3); // false
System.out.println(j4 == j5); // false
System.out.println(j5 == j6); // false

==左右两侧都是基本类型,比较的是值的大小;

==左右两侧只有一个是包装类,则将包装类转换为基本类型后比较值的大小;

==左右两侧都是包装类型,则比较的是左右两个对象的地址。

包装类型做数学运算时,需要拆箱

valueOf会先用常量池,也就是静态cache里面的对象

Long temp = (int)3.9; temp % = 2; temp最后的值是多少?

最后的结果是1

1、浮点型强转为整形会直接舍弃小数位,3.9为double类型强转为int类型的结果为3

2、使用Long类型接收int类型会自动向上转型

所以最后的结果为temp = 3 % 2,结果为1

哪些数据类型能作用在switch上?

Java 5以前:byte,short,char,int。

Java 5:enum。

Java 7:String。

String、StringBuilder、StringBuffer的区别是什么?

1、String是不可变字符串,底层是基于一个字符数组实现的,并且这个字符数组被final修饰,因此每次对String的操作都会在内存中开辟新空间,生成新对象,而StringBuilder和StringBuffer是可变字符串,修改不会生成新的对象。

2、StringBuilder是线程不安全的,因此性能较高,而StringBuffer的方法都是由synchronized修饰的,是线程安全的,但是性能较低

Java访问修饰符有哪些?以及它们的作用范围?

修饰符当前类当前包子类其他包
public
protected×
default(默认不写)××
private×××

二、类与对象

什么是面向过程?什么是面向对象?面向对象的特性?

面向过程:面向过程就是分析解决问题所需的步骤,然后使用函数一步一步地实现这些问题,然后使用时间依次调用。

面向对象:面向对象是将构成问题的事物分解为各种对象,构造对象的目的不是为了完成一个步骤,而是为了在解决整个问题的步骤中描述某个事物的行为。

面向对象的四大特征如下:

抽象:抽象就是将一类对象的共同特征总结出来构造类的过程包括数据抽象和行为抽象两方面,抽象只关注对象的哪些属性和行为,并不关注此行为的细节是什么。

封装:封装就是把不需要暴露和实现的细节隐藏起来,只对外提供最简单的接口,访问和操作数据都只能通过对外的接口操作,在Java中通过访问修饰符实现。

继承:子类继承父类的属性和方法,并且子类可以有自己独特的属性和方法,在Java中只支持单继承但是可以多重继承(即父类还可以有父类,父类的父类还可以有父类)。

多态:多态是指一种事物的多种表现形态,在编译时类型和运行时类型不一样时就是多态,多态可以屏蔽子类的差异性。

什么是方法覆盖(Override)?什么是方法重载(Overload)?

方法覆盖(Override):方法重写指子类出现了和父类名称相同的方法时会覆盖父类的方法。

方法覆盖的特点:

​ 1、覆盖的方法和被覆盖的方法的返回类型、方法名、形参列表必须相同

​ 2、覆盖的方法的访问修饰符必须大于等于被覆盖的方法

​ 3、覆盖的方法所抛出的异常必须和被覆盖方法的所抛出的异常一致,或者是其子类

方法重载(Overload):方法重载指在同一个类中可以存在多个名称相同但是形参列表不同的方法。

方法重载的特点:

​ 1、重载时通过不同的参数样式来区分。例如,不同的参数类型,不同的参数个数,不同的参数顺序

​ 2、不能通过方法的访问权限、返回类型、抛出的异常进行重载

​ 3、方法的异常类型和数目不会对重载造成影响

final 和 finally 和 finalize 的区别?

final是一个关键字,可以用来修饰变量、方法、类,被final修饰的变量不能被修改,也叫做常量;被final修饰的方法不能被重写;被final修饰的类不能被继承。

finally作为异常处理的一部分,它只能用在try/catch语句或者try/finally语句中,并且附带一个语句块,表示这段语句最终一定会被执行(不管有没有抛出异常),经常被用在需要释放资源的情况下。

finalize 是在java.lang.Object里定义的一个方法,这个方法是垃圾收集器(GC)在确定这个对象没有被引用时,在回收这个对象之前会调用它的finalize方法。

接口和抽象类有什么区别?

1、接口使用interface关键字定义,抽象类使用abstract class关键字定义

2、接口由全局常量、抽象方法(1.8之后新增了静态方法、默认方法)组成,抽象类由成员变量、构造方法、普通方法、抽象方法组成

3、接口和类是实现关系,抽象类和类是继承关系

==和equals的区别?

1、==可以用于基本数据类型和引用数据类型的比较,equals只能用于引用数据类型的比较

2、==在比较基本数据类型时比较的是值,在比较引用数据类型时比较的是地址,equals比较的时引用数据类型的值

注意:在使用equals方法比较对象时需要重写equals方法,否则使用的是Object类中的方法,Object类中的equals方法也是用的==来进行比较。

static关键字的作用

1、static修饰成员变量:static修饰的变量(静态变量)属于类,在类第一次通过类加载器到JVM时被分配内存空间。

2、static修饰成员方法:static修饰的方法属于类方法,不需要创建对象就可以调用。static方法中不能使用this和super等关键字,不能调用非static方法,只能访问所属类的静态成员变量和静态方法。

3、static修饰代码块:JVM在加载类时会执行static代码块(静态代码块),static代码块常用于初始化静态变量,static代码只会在类被加载时执行且执行一次。

4、static修饰内部类:static修饰的内部类可以不依赖外部类实例对象而被实例化,而内部类需要在外部类实例化后才能被实例化。静态内部类不能访问外部类的普通变量,只能访问外部类的静态成员变量和静态方法。

this和super关键字的作用

this:

​ 1、表示当前对象

​ 2、在本类的重载构造方法中可以通过this()调用其他的构造方法

​ 3、当局部变量和成员变量重名时,使用this关键字指定成员变量

super:

​ 1、表示父类对象

​ 2、在子类中的构造方法中可以使用super()调用父类的构造方法

​ 3、当子类属性和父类属性或方法重名时,使用super关键字进行指定父类

注意:在构造方法中this()和supper()都只能在第一行,如果没有显示的this()和super(),那么会默认有一个隐式的super()。

对象间的关系

1、依赖

概念:依赖关系表示一种使用关系,即一个类的实现需要另一个类的协助。在Java中一般表示为方法的参数需要传入另一个类的对象,就表示依赖这个类。

/**
 * 依赖关系示例
 */
public class DependencyTest {
    /**
     * 菜刀类
     */
    static class Knife {
        public void cutting(String name) {
            System.out.println("切" + name);
        }
    }

    /**
     * 厨师类
     */
    static class Chef {
        public void cutting(Knife knife, String vegetables) {
            knife.cutting(vegetables);
        }
    }

    public static void main(String[] args) {
        Chef chef = new Chef();
        chef.cutting(new Knife(), "洋葱");
    }
}
2、关联

概念:表示类与类之间的联系,它使一个类知道另一个类的属性与方法,可以是双向关联,也可以是单向关联。这种关系比依赖更强,关系也不是临时的,一般是长期的。在Java中一般表示为一个类的全局变量引用了另一个类,就表示关联了这个类。

/**
 * 关联关系示例
 */
public class AssociationTest {
    /**
     * 菜刀类
     */
    static class Knife {
        public void cutting(String name) {
            System.out.println("切" + name);
        }
    }

    /**
     * 厨师类
     */
    static class Chef {
        private Knife knife;

        public Chef(Knife knife) {
            this.knife = knife;
        }

        public void cutting(String vegetables) {
            knife.cutting(vegetables);
        }
    }

    public static void main(String[] args) {
        Chef chef = new Chef(new Knife());
        chef.cutting("西红柿");
    }
}
3、聚合

概念:聚合关系是关联关系的一种特例,是强的关联关系。聚合强调的是整体和个体之间的关系,即has-a的关系,例如汽车类与引擎类、轮胎类,以及其他零件类之间的关系。与关联关系一样,聚合关系在程序中也是通过全局变量实现,所以只能通过语义区分。但是关联关系所涉及的两个类是处于同一层次上,而在聚合关系中,两个类是处在不平等层次上的,一个代表整体,一个代表部分,就表示聚合关系。

/**
 * 聚合关系示例
 */
public class AggregationTest {
    /**
     * 汽车类
     */
    static class Car {
        /**
         * 引擎是汽车的一部分
         */
        private Engine engine;
        /**
         * 轮胎是汽车的一部分
         */
        private Tire tire;
    }

    /**
     * 引擎类
     */
    static class Engine {

    }

    /**
     * 轮胎类
     */
    static class Tire {
        
    }
}
4、组合

概念:组合关系也是关联关系的一种特例,组合是一种整体和部分的关系,即contains-a的关系,比聚合更强。组合关系的部分与整体的生命周期一致,整体的生命周期结束就意味着部分的生命周期结束,并且组合关系不能共享,在程序中也是通过一个类拥有另一个类的全局变量实现,只能通过语义区分。

/**
 * 组合关系示例
 */
public class CompositionTest {
    /**
     * 狗类
     */
    static class Dog {
        /**
         * 狗拥有头
         */
        private Head head = new Head();
        /**
         * 狗拥有腿
         */
        private Leg leg = new Leg();
    }

    /**
     * 头
     */
    static class Head {

    }

    /**
     * 腿
     */
    static class Leg {

    }
}

三、集合

Java集合体系

Java常用集合继承图如下:

在这里插入图片描述

Collection

  • List(有序可重复)
    • ArrayList:底层数据结构是数组,元素随机访问性能高,增删性能低。
    • Vector:底层数据结构是数组,元素随机访问性能高,增删性能低,是线程安全版的ArrayList。
    • LinkedList:底层数据结构是双向链表,元素随机访问性能低,增删性能高。
  • Set(无序不可重复)
    • HashSet:使用HashMap的key存储元素,判断重复依据是hashCode()和equals()。
    • TreeSet:可以排序,底层使用TreeMap的key存储元素,排序方式分为自然排序,比较器排序。

Map

  • HashMap:key的值没有顺序,key和value都可以为null,线程不安全
  • HashTable:它的key和value都不允许为null,线程安全
  • TreeMap:key的值可以自然排序,线程不安全

ArrayList和LinkedList的区别

1、ArrayList底层数据结构是数组,LinkedList底层数据结构是双向链表

2、ArrayList开辟的内存空间要求连续,LinkedList开辟的内存空间不要求连续

3、ArrayList随机访问元素性能高,因为内存连续支持索引,新增删除元素性能低,因为涉及移位操作;LinkedList随机访问元素性能低,因为查找元素需要从头查找,新增删除元素性能高,因为只需要改变链表的指针即可

ArrayList和Vector的区别

1、ArrayList线程不安全,因此效率较高;Vector线程安全,它的方法都加了synchronized关键字,因此效率较低

2、ArrayList在容量不足时会自动扩容为原来的1.5倍;Vector在容量不足时会自动扩容为原来的2倍

总的来说Vector基本可以看作是线程安全版本的ArrayList,但是在实际使用中我们基本不适用Vector,如果要实现线程安全版本的集合可以选择CopyOnWriteArrayList

HashMap和HashTable的区别

1、HashMap线程不安全,因此效率较高;HashTable线程安全,它的方法都加了synchronized关键字,因此效率较低

2、HashMap的key和value都允许为空;HashTable的key和value都不允许为空

注意:如果不考虑线程安全使用HashMap,如果要考虑线程安全使用ConcurrentHashMap

Arraylist的get、set、add、remove流程

get流程:

​ 判断index是否>=size,如果是,则抛出索引越界异常。否则返回数组的对应index位置。

// get(int index)源码分析
public E get(int index) {
    // 进行范围检测,如果大于size的值则抛出索引越界异常
	rangeCheck(index);
    
	// 返回elementData数组中指定索引的元素
	return elementData(index);
}

// 进行范围检查
private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

set流程:

​ 1、判断index是否>=size,如果是,则抛出索引越界异常

​ 2、获取传入index的旧元素

​ 3、将数组index位置的元素替换为新元素

​ 4、返回旧元素

// set(int index, E element)源码分析
public E set(int index, E element) {
    // 进行范围检测,如果大于size的值则抛出索引越界异常
    rangeCheck(index);

    // 通过索引获取旧的元素
    E oldValue = elementData(index);
    // 将索引位置设置为传入的元素
    elementData[index] = element;
    // 返回旧的元素
    return oldValue;
}

// 进行范围检查
private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

add流程:

​ 1、确保数组长度足够

​ 2、判断是否需要扩容,如果需要扩容那么扩容为原来的1.5倍

​ 3、将元素添加到数组中,并且size自增1

// add(E e)源码分析
public boolean add(E e) {
    /** 
     * 确保数组长度足够,size是数组中数据的个数,因为要添加一个元素,所以size+1,
     * 先判断size+1的长度数组能否放得下
     */
	ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 将元素放在数组中size++的位置
	elementData[size++] = e;
	return true;
}

// 确保数组长度方法
private void ensureCapacityInternal(int minCapacity) {
    // 判断数组是否是空数组
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 如果是空数组那么最小容量为默认大小(10)或者本次添加元素需要的最小容量中的最大值
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    /**
     * 确保显示容量,在这个方法中进行modCount++操作,表示结构发生了变化
     * 并且判断是否需要进行扩容操作
     */
    ensureExplicitCapacity(minCapacity);
}

// 确保显示容量方法
private void ensureExplicitCapacity(int minCapacity) {
    // 结构变化数量自增
    modCount++;

    // 判断是否需要扩容
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        // 进行扩容操作
        grow(minCapacity);
}

// 扩容核心方法
private void grow(int minCapacity) {
   // 将扩充前的elementData大小给oldCapacity
   // overflow-conscious code
   int oldCapacity = elementData.length;
    
   // 新容量newCapacity是1.5倍的旧容量oldCapacity
   int newCapacity = oldCapacity + (oldCapacity >> 1);
    
   /**
    * 这句话就是适应于elementData就空数组的时候,length=0,那么oldCapacity=0,
    * newCapacity=0,所以这个判断成立,在这里就是真正的初始化elementData的大小了,就是为10。
    */
   if (newCapacity - minCapacity < 0)
       newCapacity = minCapacity;
    
   // 如果newCapacity超过了最大的容量限制,就调用hugeCapacity,也就是将能给的最大值给newCapacity
   if (newCapacity - MAX_ARRAY_SIZE > 0)
       newCapacity = hugeCapacity(minCapacity);
    
   // 新的容量大小已经确定好了,就copy数组,改变容量大小。
   // minCapacity is usually close to size, so this is a win:
   elementData = Arrays.copyOf(elementData, newCapacity);
}

// 这个就是上面用到的方法,很简单,就是用来赋最大值。
private static int hugeCapacity(int minCapacity) {
   if (minCapacity < 0) // overflow
       throw new OutOfMemoryError();
    
   /**
    * 如果minCapacity都大于MAX_ARRAY_SIZE,那么就Integer.MAX_VALUE返回,
    * 反之将MAX_ARRAY_SIZE返回。因为maxCapacity是三倍的minCapacity,可能扩充的太大了,
    * 就用minCapacity来判断了。
    * Integer.MAX_VALUE:2147483647   MAX_ARRAY_SIZE:2147483639
    * 也就是说最大也就能给到第一个数值。还是超过了这个限制,就要溢出了。相当于arraylist给了两层防护。
    */
   return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}

remove流程:

​ 1、进行范围检测

​ 2、通过索引获取要删除的元素

​ 3、计算要移动的元素个数

​ 4、将元素挨个往前移

​ 5、将原本的组后一个元素赋值为null

​ 6、返回被移除的元素

// remove(int index)源码分析
public E remove(int index) {
    // 进行范围检测,如果大于size的值则抛出索引越界异常
	rangeCheck(index);
    // 结构变化数自增
	modCount++;
    // 通过索引获取要删除的元素
	E oldValue = elementData(index);
    // 计算要移动的元素个数
	int numMoved = size - index - 1;
    
    // 判断是否有需要移动的元素
	if (numMoved > 0)
        // 将被删除元素后的元素挨个向前移动
		System.arraycopy(elementData, index+1, elementData, index, numMoved);
    
	// 将元素--size的位置设置为null,让GC可以更快回收它
	elementData[--size的位置设置为null,让GC可以更快回收它] = null; 
	// 返回被删除的元素
	return oldValue;
}

// 进行范围检查
private void rangeCheck(int index) {
    // 如果超过数组中数据的最大长度就抛出索引越界异常
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

HashSet如何检查重复?HashSet是如何保证数据不可重复的?

HashSet内部维护了一个HashMap类型的属性,HashSet使用add方法添加元素实际上是调用的HashMap的put方法,所以HashSet的值就是一个HashMap的key。它判断元素是否存在的依据,不仅要比较hash值,还要结合equles方法比较。

// HashSet部分源码

private static final Object PRESENT = new Object();
private transient HashMap<E,Object> map;

public HashSet() {
    map = new HashMap<>();
}

public boolean add(E e) {
    // 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值
	return map.put(e, PRESENT)==null;
}

public boolean remove(Object o) {
    // 调用HashMap的remove方法
    return map.remove(o)==PRESENT;
}

HashMap的实现原理,在JDK1.7和JDK1.8中有哪些不同?

在Java中,保存数据有两种初始的比较简单的数据结构,分别是数组链表。**数组的特点是:内存连续,拥有下标,所以随机访问效率高,但是添加删除相对慢。链表的特点是:内存不连续,随机访问效率低,但是添加删除效率高。**HashMap就是由这两种结构所结合,发挥各自的优势使用拉链法来解决hash冲突来实现的。

在JDK1.8之前解决Hash冲突:使用的数据结构为数组+链表的方式,如果发生hash冲突那么使用拉链法解决冲突,将发生冲突的key加入到链表中即可。

在这里插入图片描述

在JDK1.8之后解决Hash冲突:相对于之前的版本,JDK1.8在解决hash冲突时有了较大的变化,当链表长度大于某个阈值(默认为8),如果数组长度小于64,那么会就先进行扩容,否则就会将链表转换成红黑树,以减少搜索时间。

在这里插入图片描述

JDK1.7与JDK1.8的主要不同:

不同点JDK1.7JDK1.8
存储结构数组 + 链表数组 + 链表 + 红黑树
初始化方式单独函数:inflateTable()直接集成到了扩容函数resize()
hash值计算方式扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算
存放数据的规则无冲突时,存放数组;冲突时,存放链表无冲突时,存放数组;冲突 & 链表长度 < 8:存放单链表;冲突 & 链表长度 > 8 & 数组长度 < 64,扩容;冲突 & 数组长度 > 64:链表树化并存放红黑树
插入数据方式头插法(先将原位置的数据都向后移动1位,再插入数据到头位置)尾插法(直接插入到链表尾部/红黑树)
扩容后存储位置的计算方式全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1))按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量)

HashMap的get方法具体流程

// get方法源码分析
public V get(Object key) {
    Node<K,V> e;
    // 判断获取的节点是否为null,为null就返回null,否则返回节点的value值
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

// 获取节点具体方法
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 判断数组是否有值,如果有值则判断通过key的hash码获取对应索引的元素是否为null
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 通过hash和equals方法判断是否是要找的节点,如果是则直接返回
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 判断该节点是否有下一个节点
        if ((e = first.next) != null) {
            // 判断是否属于树节点
            if (first instanceof TreeNode)
                // 获取并返回对应的树节点
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            // 遍历链表,通过hash和equals方法判断每一个元素是否相等,如果找到则直接返回
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

HashMap的put方法具体流程

当我们put的时候,首先计算 keyhash值,这里调用了 hash方法,hash方法实际是让key.hashCode()key.hashCode()>>>16进行异或操作,高16bit补0,一个数和0异或不变,所以 hash 函数的作用就是:高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞。按照函数注释,因为bucket数组大小是2的幂,计算下标index = (table.length - 1) & hash,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位,为了减少散列的碰撞,设计者综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中用了时间复杂度 为O(logN) 的树结构来提升碰撞下的性能。

// HashMap put(K key, V value)源码分析
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

// 计算hash值的方法
static final int hash(Object key) {
    int h;
    /**
     * 如果key为null,则返回0,否则使用key的hashCode和hashCode无符号右移16位
     * 后的值进行异或操作,由于右移16位之后,高16位补0,低16位就等于原来的高16位,
     * 并且一个数和0异或的结果不变,最后的计算结果就相当于原本的高16位不变,低16位
     * 与高16位做了一次异或操作
     */
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

// put元素的方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;

    // 判断数组是否为空
    if ((tab = table) == null || (n = tab.length) == 0)
        /**
         * 如果数组为空则调用resize()方法进行初始化数组操作,
         * 并且将初始化后的数组赋值给变量tab,将数组长度赋值给变量n
         */
        n = (tab = resize()).length;

    /**
     * 将数组长度-1的值与key的hash值进行按位与运算,算出一个索引值
     * 将数组指定索引位置的元素赋值给变量p,并且判断该元素是否为null
     */
    if ((p = tab[i = (n - 1) & hash]) == null)
        /**
         * 如果指定索引位置元素为null,则代表该索引位置还没有元素
         * 通过newNode方法创建一个Node元素并且赋值给数组指定索引位置
         */
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        /**
         * 通过将指定下标元素的hash值和本次插入元素的hash值进行比较,如果hash值相同
         * 再比较key的地址或者key的值是否相同,如果相同则代表元素重复,将元素赋值给变量e
         */ 
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        
        // 判断节点是否属于红黑树节点
        else if (p instanceof TreeNode)
            // 如果是红黑树节点,那么调用putTreeVal方法将元素添加到红黑树,返回可能为null
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        
        // 如果hash值一样并且元素不属于红黑树节点,那么属于链表节点
        else {
            // 开始遍历链表
            for (int binCount = 0; ; ++binCount) {
                // 判断是否是链表最后一个节点
                if ((e = p.next) == null) {
                    // 如果是最后一个节点,那么将元素插入到链表末尾
                    p.next = newNode(hash, key, value, null);
                    // 判断链表长度是否大于8
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        /**
                         * 调用treeifyBin方法将链表尝试转换为红黑树,在该方法内部判断了数组长度
                         * 是否大于64,如果没有大于64,那么调用resize方法进行数组扩容,
                         * 否则将链表转换为红黑树
                         */
                        treeifyBin(tab, hash);
                    // 退出循环
                    break;
                }
                // 如果不是最后一个节点,那么通过hash和equals方法判断key是否相同
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // key相同直接退出循环
                    break;
                
                // 将下一个节点赋值给变量p
                p = e;
            }
        }
        
        // 判断key是否是重复,当e不等于null时代表key重复
        if (e != null) { // existing mapping for key
            // 将之前的value取出
            V oldValue = e.value;
            // 判断onlyIfAbsent是否为false或者旧的值是否为null
            if (!onlyIfAbsent || oldValue == null)
                // 用新的值替换旧的值
                e.value = value;
            // 访问后回调,HashMap中为空方法,用于LinkedHashMap的回调操作
            afterNodeAccess(e);
            // 返回旧的值
            return oldValue;
        }
    }
    // 结构改变计数器自增
    ++modCount;
    // 长度自增,并且判断数组长度是否达到下一次需要扩容的长度threshold = 容量 * 负载系数(默认0.75)
    if (++size > threshold)
        // 进行扩容
        resize();
    // 节点插入后的处理,HashMap中为空方法,用于LinkedHashMap的回调操作
    afterNodeInsertion(evict);
    return null;
}

HashMap是如何实现扩容的?为什么每次扩容后的长度都是2的幂次方?

在JDK1.8中,HashMap使用resize()方法进行扩容,并且每一次扩容后的HashMap长度都是2的幂次方,这是由于为了让HashMap存取高效,尽量减少碰撞,就需要将数据分配均匀。但是Hash值的取值范围是int类型的取值范围(-2147483648 到 2147483647),但是我们不可能一次创建一个四十多亿长度的数组,内存是放不下的,所以我们只能用得到的Hash值进行数组长度取模运算(比如hash%length),得到的余数就是数据存放的数组的下标位置。但是当数组长度为2的幂次方时,取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的二进制与(&)操作,也就是说hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方,并且采用二进制位操作&,相对于直接使用%取余能够提高运算效率,这也就是为什么Hash Map的长度是2的幂次方

// HashMap扩容方法
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    // 获取旧数组的容量
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 旧数组的临界点,当元素个数达到临界点就会触发扩容
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        // 判断旧的容量是否大于等于最大容量
        if (oldCap >= MAXIMUM_CAPACITY) {
            // 如果旧的容量大于等于最大容量就将临界点设置为int类型的最大值
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 否则判断新容量(旧容量的2倍)是否小于最大容量并且旧容量大于等于默认容量
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 将旧的临界点扩大两倍作为新的临界点
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        // 如果旧的容量和旧的临界点都不大于零,那么将新的容量和临界点设置初始值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        // 遍历旧的数组
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            // 判断当前遍历节点是否有值
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                // 判断该节点是否是链表
                if (e.next == null)
                    // 如果不是链表就直接将该节点重新计算索引并赋值
                    newTab[e.hash & (newCap - 1)] = e;
                // 判断是否是树节点
                else if (e instanceof TreeNode)
                    // 进行树节点分割
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    /**
                     * 进入该代码块表示当前节点为链表节点,进行扩容后
                     * 会将原来的链表进行拆分,拆分为低位链表和高位链表
                     */
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        /**
                         * 将节点的hash值和旧数组的容量进行&运算,判断结果是否为0
                         * 如果结果为0那么将该节点元素添加到低位链表loHead,loTail
                         * 用于维护原链表的节点顺序,如果结果不为0,那么将该节点添
                         * 加到高位链表hiHead,hiTail也用于维护原链表的节点顺序
                         */ 
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 判断低位链表的尾节点是否为null
                    if (loTail != null) {
                        loTail.next = null;
                        // 将低位链表设置到新数组的对应索引位置
                        newTab[j] = loHead;
                    }
                    // 判断高位链表尾节点是否为null
                    if (hiTail != null) {
                        hiTail.next = null;
                        // 将高位链表设置到新数组中原节点索引位置加上旧容量的位置
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

四、Java8新特性

Lambda表达式

Lambda并不是一个什么的缩写,而是希腊第十一个字母λ的读音,同时也是微积分函数中的一个概念,所表达的意思是一个函数入参和出参定义,在编程语言中其实是借用了数学中的 λ,并且多了一点含义,在编程语言中功能代表它具体功能的叫法是匿名函数(Anonymous Function),lambda 表达式是一小段代码,它接受参数并返回一个值。Lambda 表达式类似于方法,但它们不需要名称,并且可以直接在方法体中实现。

Java中Lambda表达式的语法为:

/**
 * (...)表示方法的参数,如果没有参数可直接写为(),如果只有一个参数括号可以省略
 * 例如(param1) -> {// code}可以简写为:param1 -> {// code},其余情况均不可省略
 * 
 * -> 为Lambda表达式固定写法
 * 
 * {...}表示方法体,如果方法体只有一行代码大括号可以省略,例如
 * param1 -> {System.out.println(param1);}可以简写为:
 * param1 -> System.out.println(param1);
 *
 * Lambda表达式支持有返回值,使用return关键字返回即可
 */
(param1, param2[,...]) -> {
    // code block
    // [return]
}

函数式接口

有且只有一个抽象方法的接口被称为函数式接口,函数式接口适用于函数式编程的场景,Lambda就是Java中函数式编程的体现,换句话说就是只有函数式接口才可以使用Lambda表达式。使用Lambda表达式可以创建一个函数式接口的对象,一定要确保接口中有且只有一个抽象方法,这样Lambda才能顺利的进行推导。

在Java8中专门为函数式接口添加了一个注解@FunctionalInterface,这个注解的作用与@Override 注解的作用类似,@Override 注解用于检查该方法是否为覆写,而@FunctionalInterface注解可用于一个接口的定义上,一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法(equal和hashcode方法不算),否则将会报错。但是这个注解不是必须的,只要符合函数式接口的定义,那么这个接口就是函数式接口。

// Runnable接口就是一个函数式接口
@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

// 使用匿名函数方式
new Thread(new Runnable() {
    @Override
    public void run() {
        // do something
    }
});

// 使用Lambda表达式简化原匿名函数
new Thread(() -> {
    // do something
});

方法引用

方法引用是一种直接引用已经存在的方法的方式,它允许我们在代码中通过方法的名称来引用方法。方法引用可以看作是Lambda表达式的一种简化形式,它提供了一种更加简洁的方式来实现函数式接口。

方法引用的语法由两部分组成:类名或对象名和方法名,中间使用双冒号(::)进行分割,根据方法引用的情况,可以分为以下几种形式:

  • 静态方法引用:类名::静态方法名

    // System.out.println(); => System.out::printf
    Consumer<String> consumer = System.out::printf;
    
  • 实例方法引用:对象名::实例方法名

    // list.add() => list::add
    List<String> list = Collections.emptyList();
    Consumer<String> consumer = list::add;
    
  • 构造方法引用:类名::new

    // new ArrayList() => ArrayList::new
    Supplier<List<String>> s = ArrayList::new;
    

Stream

Stream,就是JDK8又依托于函数式编程特性为集合类库做的一个类库,它其实就是jdk提供的函数式接口的最佳实践。它能让我们通过lambda表达式更简明扼要的以流水线的方式去处理集合内的数据,可以很轻松的完成诸如:过滤、分组、收集、归约这类操作。

其中Stream的操作大致分为两类

  • 中间型操作:每次返回一个新的流,可以有多个。
  • 终端型操作:每个流只能进行一次终端操作,终端操作结束后流无法再次使用。终端操作会产生一个新的集合或值。

另外,Stream有几个特性:

  1. stream不存储数据,而是按照特定的规则对数据进行计算,一般会输出结果。
  2. stream不会改变数据源,通常情况下会产生一个新的集合或一个值。
  3. stream具有延迟执行特性,只有调用终端操作时,中间操作才会执行。

关于Stream的详细使用可查看链接:https://blog.csdn.net/mu_wind/article/details/109516995?spm=1001.2014.3001.5506

Optional

Optional是Java8中引入的一个用来解决空指针(NullPointException)的一个类。本质上,这是一个包含有可选值的包装类,这意味着 Optional 类既可以含有对象也可以为空,所以其实使用Optional并不能完全杜绝空指针,只是通过合理使用Optional来优雅的减少空指针。

创建Optional实例

Optional的构造方法都是被私有化的,所以要获得Optional的对象是通过它提供的三个静态方法,分别是:

  • empty():用于构建一个空的Optional对象

    Optional<User> optional = Optional.empty();
    
  • of(T value):用于构建一个有值的Optional对象,如果传入的value为null则会抛出空指针异常

    Optional<User> optional = Optional.of(user);
    
  • ofNullable(T value) 用于构建一个有值的Optional对象,如果传入的value为null,则创建一个空的Optional对象,所以如果对象即可能是null也可能是非 null,你就应该使用ofNullable()方法

    Optional<User> optional = Optional.ofNullable(user);
    

访问Optional的值

从Optional实例中取回实际值对象的方法之一是使用get()方法,不过get()方法在值为null的情况下会抛出NoSuchElementException异常,所以要避免异常通常需要先验证是否有值,Optional提供了ifPresent()方法来校验值是否为null。

除了使用get()方法获取值之外,还可以使用orElse()或者orElseGet()是其如果Optional的值为null的时候返回指定的默认值,orElse()方法和orElseGet()方法的主要区别是如果Optional的值不为null,还是会执行orElse(),但是orElseGet()方法不会被执行。

class User {
    private String name;

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

public static void main(String[] args) {
    User user = new User("tom");
    
    System.out.println("orElse");
    User orElse = Optional.ofNullable(user).orElse(createUser("jack"));
    
    System.out.println("orElseGet");
    User orElseGet = Optional.ofNullable(user).orElseGet(() -> createUser("jack"));
}

public static User createUser(String name) {
    System.out.println("Create User");
    return new User(name);
}

// 执行结果为
orElse
Create User
orElseGet

除了orElse()方法和orElseGet()方法之外,Optional还提供了orElseThrow(),用于在值为null的时候抛出指定的异常而不是备选的返回值,这个方法让我们有更丰富的语义,可以决定抛出什么样的异常,而不总是抛出NullPointerException

// 这里如果user为null,就会抛出IllegalArgumentException异常
User orElseThrow = Optional.ofNullable(user).orElseThrow(() -> new IllegalArgumentException());

五、I/O流

I/O流简介

I/O即Input/Output的缩写,意为输入/输出,用来做数据读取和写入操作。可以理解为对文件的写入和读取,但是在Java中对这种操作叫做对流的操作,而流不只是可以对文件进行读写,还可以对网络、内存等进行操作。

编码&字节&字符

在对流的具体操作之前,我们需要先了解关于编码、字节、字符之间的关系。

关于字节:在计算机中的数据存储最小单位是bit(二进制位,只有0和1),而一个字节(Byte)为8个bit

关于字符:字符表示一个英文字母或者一个汉字

然而在不同的编码方式中一个字符占用的字节数也不同,下面的表格为常用编码对应的字符占用字节数

编码方式英文字符中文字符
GB2312、GBK12
UTF-813~4
UTF-1623~4

I/O流的分类

  • 按照流对象不同可以划分为字节流、字符流

    字节流指的是流的操作对象是字节,也就是byte类型的数据,字符流指的是流的操作对象是字符,也就是char类型的数据

  • 按照流方向不同可以划分为输入流、输出流

    输入流指的是从文件读取到程序的流,输出流指的是从程序写入到文件的流,流的方向要以当前的程序的视角来看

  • 按照处理方式不同可以划分为节点流、处理流

    节点流指的是与数据源或目标进行连接的流,它们提供了对原始数据源或目标的直接访问,例如FileInputStream和FileOutputStream等。而处理流是对节点流的包装,也称为包装流,它们提供了额外的功能,如缓冲、压缩、加密等,以便更方便地处理数据,例如BufferedInputStream、BufferedOutputStream等

在Java中所有的流都是从以下四个抽象类派生

  • InputStream(字节输入流)
  • OutputStream(字节输出流)
  • Reader(字符输入流)
  • Writer(字符输出流)

File

File类是Java中专门用于操作文件和目录的类,可以通过指定的绝对路径或者相对路径来操作一个文件或者目录。

File类的常用方法:

  • canRead:判断文件或目录是否有可读权限
  • canWrite:判断文件或目录是否有可写权限
  • createNewFile:创建一个新的文件或目录
  • delete:删除指定的文件或目录
  • exists:判断文件或目录是否存在
  • getAbsolutePath:获取文件或目录的绝对路径
  • getName:获取文件名
  • isFile:判断是否是一个文件
  • length:文件的字节大小
  • mkdir:创建一个目录
  • mkdirs:创建多级目录

File类代码示例:

public static void main(String[] args) throws IOException {
    File file = new File("src\\main\\resources\\file.txt");

    String name = file.getName();
    long length = file.length();
    boolean canRead = file.canRead();
    boolean canWrite = file.canWrite();
    String absolutePath = file.getAbsolutePath();
    boolean exists = file.exists();
    boolean isFile = file.isFile();
    System.out.println("文件名:" + name +
                       "\n文件大小:" + length +
                       "个字节\n是否可读:" + canRead +
                       "\n是否可写:" + canWrite +
                       "\n绝对路径:" + absolutePath +
                       "\n文件是否存在:" + exists +
                       "\n是否是一个文件:" + isFile);
}

// 输出结果为:
// 文件名:file.txt
// 文件大小:30个字节
// 是否可读:true
// 是否可写:true
// 绝对路径:D:\IdeaProjects\practice-demo\src\main\resources\file.txt
// 文件是否存在:true
// 是否是一个文件:true

InputStream(字节输入流)

InputStream(字节输入流)是用于从数据源(文件、网络等)读取数据的字节信息到内存中,在Java中InputStream是所有的字节输入流的基类。

InputStream常用方法:

  • read():返回输入流中下一个字节的数据。返回的值介于 -1 到 255 之间。只有当未读取任何字节时,才返回 -1 ,表示文件结束。

  • read(byte b[ ]) : 从输入流中读取一些字节存储到数组 b 中。如果数组 b 的长度为零,则不读取。如果没有可用字节读取,那么返回 -1。如果有可用字节读取,则最多读取的字节数最多等于 b.length , 返回读取的字节数。这个方法等价于 read(b, 0, b.length)

  • read(byte b[], int off, int len):在read(byte b[ ]) 方法的基础上增加了 off 参数(偏移量)和 len 参数(要读取的最大字节数)。

  • skip(long n):忽略输入流中的 n 个字节 ,返回实际忽略的字节数。

  • available():返回输入流中可以读取的字节数。

  • close():关闭输入流释放相关的系统资源

InputStream类代码示例:

public static void main(String[] args) {
    try (InputStream fis = new FileInputStream("src\\main\\resources\\file.txt")) {
        System.out.println("流中可读取的字节数:" + fis.available());
        int read;
        long skip = fis.skip(3);
        System.out.println("跳过的字节数:" + skip);
        System.out.print("流中读取的文件内容为:");
        while ((read = fis.read()) != -1) {
            System.out.print((char) read);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

// 输出结果为:
// 流中可读取的字节数:12
// 跳过的字节数:3
// 流中读取的文件内容为:lo World!

file.txt文件中的原始数据为:

在这里插入图片描述

OutputStream(字节输入流)

OutputStream(字节输出流)是用于将数据写入到目标(通常是磁盘或者网络),在Java中OutputStream是所有的字节输出流的基类。

OutputStream的常用方法:

  • write(int b):将特定字节写入输出流。

  • write(byte b[ ]) : 将字节数组b 写入到输出流,等价于 write(b, 0, b.length)

  • write(byte[] b, int off, int len) : 在write(byte b[ ]) 方法的基础上增加了 off 参数(偏移量)和 len 参数(要读取的最大字节数),代表从数组boff的位置开始写入len个字节长度。

  • flush():刷新此输出流并强制写出所有缓冲的输出字节。

  • close():关闭输出流释放相关的系统资源。

public static void main(String[] args) {
    try (OutputStream fos = new FileOutputStream("src\\main\\resources\\out.txt")) {
        String str = "Never give up!";
        
        // 将数据写入到流中
        fos.write(str.getBytes());
    } catch (IOException e) {
        e.printStackTrace();
    }
}

运行结果:

在这里插入图片描述

不过在通常情况下,我们一般会使用高级流来包裹低级流,像FileInputStream、FileOutputStream一般就会搭配BufferedInputStream、BufferedOutputStream缓冲流来使用,下面我们来学习一下缓冲流的使用方法,非常的简单

public static void main(String[] args) {
    try (FileInputStream fis = new FileInputStream("src\\main\\resources\\file.txt");
         // 创建缓冲输入流
         BufferedInputStream bis = new BufferedInputStream(fis);
         // 使用追加写模式,否则会将源文件内容覆盖(实际上是在创建好输出流时就会清空文件中的信息)
         FileOutputStream fos = new FileOutputStream("src\\main\\resources\\out.txt", true);
         // 创建缓冲输出流
         BufferedOutputStream bos = new BufferedOutputStream(fos)) {

        byte[] buffer = new byte[1024];
        // 遍历将file.txt写入到out.txt文件中
        while (bis.read(buffer) != -1) {
            bos.write(buffer);
        }

    } catch (IOException e) {
        e.printStackTrace();
    }
}

运行结果:

在这里插入图片描述

Reader(字符输入流)

Reader(字符输入流)是用于从源头读取数据(字符信息)到内存中,在Java中Reader是所有字符输入流的基类。

Reader的常用方法:

  • read() : 从输入流读取一个字符。

  • read(char[] cbuf) : 从输入流中读取一些字符,并将它们存储到字符数组 cbuf中,等价于 read(cbuf, 0, cbuf.length)

  • read(char[] cbuf, int off, int len):在read(char[] cbuf) 方法的基础上增加了 off 参数(偏移量)和 len 参数(要读取的最大字符数),代表从数组cbuf中的off的位置开始读len个字符。

  • skip(long n):忽略输入流中的 n 个字符 ,返回实际忽略的字符数。

  • close() : 关闭输入流并释放相关的系统资源。

public static void main(String[] args) {
    try (
        // 创建一个文件字符输入流
        FileReader fr = new FileReader("src\\main\\resources\\file.txt");
    ) {
        int read;
        long skip = fr.skip(3);
        System.out.println("跳过的字节数:" + skip);
        System.out.print("流中读取的文件内容为:");

        while ((read = fr.read()) != -1) {
            System.out.print((char) read);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

// 输出结果为:
// 跳过的字节数:1
// 流中读取的文件内容为:好 World!

file.txt文件中的原始数据为:

在这里插入图片描述

Writer(字符输出流)

Writer(字符输出流)是用于将数据(字符信息)写入到目标(通常是文件)中,在Java中Writer是所有字符输出流的基类。

Writer的常用方法:

  • write(int c) : 写入单个字符。

  • write(char[] cbuf):写入字符数组 cbuf,等价于write(cbuf, 0, cbuf.length)

  • write(char[] cbuf, int off, int len):在write(char[] cbuf) 方法的基础上增加了 off 参数(偏移量)和 len 参数(要读取的最大字符数)。

  • write(String str):写入字符串,等价于 write(str, 0, str.length())

  • write(String str, int off, int len):在write(String str) 方法的基础上增加了 off 参数(偏移量)和 len 参数(要读取的最大字符数)。

  • append(CharSequence csq):将指定的字符序列附加到指定的 Writer 对象并返回该 Writer 对象。

  • append(char c):将指定的字符附加到指定的 Writer 对象并返回该 Writer 对象。

  • flush():刷新此输出流并强制写出所有缓冲的输出字符。

  • close():关闭输出流释放相关的系统资源。

public static void main(String[] args) {
    try (FileWriter fw = new FileWriter("src\\main\\resources\\out.txt")) {
        // 向文件中写入数据
        fw.write("我爱Java,Java爱我!");
    } catch (IOException e) {
        e.printStackTrace();
    }
}

运行结果:

在这里插入图片描述

关于字符缓冲流BufferedReader和BufferedWriter的用法和字节缓冲流类似,内部都是维护了一个数组作为缓冲区,不过一个操作的是字符一个操作的是字节。

并且需要注意的是在读取除了文本信息以外的数据(图片、视频、音频等)都需要使用字节流,如果读取纯文本数据,那么字符流的效率更高。

I/O模型详解

I/O整体来说是一个复杂、抽象、庞大的知识模块,是很多人学习Java中一个痛点,接下来我们将站在I/O模型的角度来继续更深层次的探索I/O、理解I/O。

在Java中有三种常见的I/O模型,分别为:BIONIOAIO,在具体介绍这三种I/O模型之前,我们先来回顾一下同步与异步、阻塞与非阻塞的概念,因为要想更好的理解I/O这几个概念非常重要。

同步: 同步就是调用方发起一个请求,在被调用方处理完毕之后才返回,被调用方未处理完请求之前,调用不返回。

异步:异步就是调用方发起一个请求,立刻得到被调用方的响应表示已接收到请求,但是被调用方并没有返回请求处理结果,此时我们可以处理其他的请求,被调用方可以通过事件和回调等机制来通知调用者其返回结果。

阻塞:阻塞就是调用方发起请求后,调用方一直等待被调用方的处理结果,此时调用方一直处于等待返回结果的状态而无法处理其它事务。

非阻塞:非阻塞就是调用方发起请求后,调用方不需要一直等待被调用方的处理结果,可以先去处理其它事务。

如果还是认为这些概念太过于抽象难以理解,那么下面我将用生活中的例子来帮助理解这几个概念,这里我们以银行取款为例

同步:自己亲自出马持银行卡到银行取钱(使用同步IO时,Java自己处理IO读写)

异步:委托一个小弟拿银行卡到银行取钱,然后给你(使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS(银行卡和密码),OS需要支持异步IO操作API)

阻塞:ATM排队取款,你只能等待(使用阻塞IO时,Java调用会一直阻塞到读写完成才返回)

非阻塞:柜台取款,取个号,然后坐在椅子上做其它事,等号广播会通知你办理,没到号你就不能去,你可以不断问大堂经理排到了没有,大堂经理如果说还没到你就不能去(使用非阻塞IO时,如果不能读写Java调用会马上返回,当IO事件分发器会通知可读写时再继续进行读写,不断循环直到读写完成)

到这里是不是对同步与异步、阻塞与非阻塞的概念有一个更具体的认识了,接下来我们再对这几种I/O模型来进行详细的说明

BIO(Blocking I/O)

BIO被称为同步阻塞IO,也就是我们平时使用的传统IO,它基于流模型实现,一个连接一个线程,客户端有连接请求时,服务器端就需要启动一个线程进行处理,线程开销较大。当然这一点可以使用线程池进行优化,但是线程依旧是宝贵的资源。所以BIO的特点是模式简单使用方便,但并发处理能力低,容易成为应用性能的瓶颈。BIO是面向流的,BIO的Stream是单向的。

很多时候,人们也把 java.net 包下的部分网络 API,比如 Socket、ServerSocket、HttpURLConnection 也归类到同步阻塞 IO,因为网络通信同样是 IO 行为。

以下是以Client/Server为例的BIO模型示意图:

在这里插入图片描述

NIO(Non-Blocking I/O)

NIO被称为同步非阻塞IO,在Java中于Java1.4中引入,是传统 IO 的升级,提供了 Channel、Selector、Buffer 等新的抽象,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。Mina2.0和Netty5.0网络通信框架都是通过NIO实现的网络通信。NIO是面向缓冲区的,NIO的channel是双向的。

以下是以Client/Server为例的NIO模型示意图:

在这里插入图片描述

**关于NIO的深入学习可参考文章:**http://www.52im.net/thread-2640-1-1.html

AIO(Asynchronous I/O)

AIO被称为异步非阻塞IO,也叫做NIO2.0,是 NIO 的升级,异步 IO 的操作基于事件和回调机制,性能是最好的。底层实现是通过epoll的I/O多路复用机制,但是目前应用并不广泛也就不必多做介绍。

六、异常

在编程的过程中异常是不可避免地,在Java中程序遇到异常的情况时,Java会抛出异常对象来指示发生了什么问题,了解常见的Java异常类型以及如何处理它们,是编写健壮和可靠代码的关键,接下来我们就来详细的学习Java的异常以及处理方法。

在Java中的异常一般可分为编译时异常运行时异常两大类,也可以被称为受检异常和非受检异常,这二者最大的区别在于编译时异常必须要进行处理否则会编译失败,而至于处理方式则是要么抛出,要么使用try-catch语句对异常进行自定义处理。

编译时异常

编译时异常通常指的是Exception类下的所有除了RuntimeException及其子类的其他异常类,这里列举几个比较常见的编译时异常:

  • IOException(IO异常):当发生输入或输出操作失败时,比如文件读写错误或网络连接问题,会抛出IOException。
  • ClassNotFoundException(类找不到异常):当JVM试图加载某个类时,但找不到该类时,会抛出ClassNotFoundException。
  • SQLException(SQL异常):当在数据库操作中遇到错误时抛出的异常。例如,执行SQL查询或更新操作时可能发生的异常。
运行时异常

运行时异常通常指的是RuntimeException及其所有子类,这列举几个比较常见的运行时异常:

  • NullPointerException(空指针异常):当尝试访问一个空引用对象时抛出的异常。这通常是因为没有为引用赋值或引用对象已被销毁。

  • ArithmeticException(算术异常):当出现算术错误时抛出的异常,如除零操作或非法的数学计算。

  • ArrayIndexOutOfBoundsException(数组越界异常):当尝试访问数组中不存在的索引位置时抛出的异常。例如,使用一个超出数组长度的索引进行数组访问。

自定义异常

除了Java库中已经提供了的标准异常之外,我们还可以根据需求创建自定义异常。在Java中要自定义一个异常的步骤非常简单,如果需要自定义一个编译时异常,那么可以通过继承Exception来实现,如果需要自定义一个运行时异常,那么可以通过继承RuntimeException来实现。

// 通过继承Exception实现自定义编译时异常
public class MyException extends Exception {
    public MyException(String exceptionMsg) {
        super(exceptionMsg);
    }

	// 必须强制要求处理,否则编译不通过
    public static void main(String[] args) throws MyException {
        throw new MyException("抛出自定义编译时异常");
    }
}


// 通过继承RuntimeException实现自定义运行时异常
public class MyRuntimeException extends RuntimeException {
    public MyRuntimeException(String exceptionMsg) {
        super(exceptionMsg);
    }
	
	// 可以不用处理
    public static void main(String[] args) {
        throw new MyRuntimeException("抛出自定义运行时时异常");
    }
}
如何处理异常

在Java中处理异常的方式分为两种

  • 使用throws关键字抛出
  • 使用try-catch进行捕获并自行处理
// 使用throws关键字抛出异常
public void someMethod() throws MyException {
	
}

/*
 * 使用try-catch捕获并处理异常,是指在try代码块中的代码如果发生异常
 * 那么就会执行catch代码块中的代码,一般try-catch代码块还会搭配finally
 * 代码块使用,finally代码块的代码不管是否发生异常都会执行,一般用于释放资源
 */
public void someMethod() {
    try {
        // 可能会抛出异常的代码
    } catch (MyException e) {
        // 异常处理代码
    } finally {
        // 无论是否异常都会执行的代码
    }
}

至于具体是选择抛出异常还是选择捕获异常自行处理要视情况而定,一般来说遵循以下的原则:

  1. 捕获异常:

    • 当你能够预见并处理某个特定类型的异常时,应该捕获该异常并采取适当的处理措施。

    • 当你希望继续执行后续操作而不是中断程序流程时,可以捕获异常并进行处理。这样可以确保程序的正常执行。

    • 当异常的处理逻辑与当前方法相关并可以在方法内部解决时,应该捕获异常。例如,可以向用户显示错误消息或记录异常日志。

  2. 抛出异常:

    • 当当前方法无法处理某个异常,或者异常超出当前方法的职责范围时,应该将异常抛出给调用者。调用者将负责处理异常。
    • 当异常的处理逻辑与当前方法无关,并且需要由上层调用者决定如何处理时,应该抛出异常。
    • 当异常表示了一个系统级错误或严重问题,无法在当前方法内部进行恢复时,应该抛出异常。例如,数据库连接失败或无法读取配置文件等情况。

总的原则是,在能够恢复或处理异常的情况下,应该捕获异常。而在无法处理异常或异常超出当前方法职责的情况下,应该抛出异常给上层调用者处理。

需要注意的是,不应该滥用异常。只有在异常情况下才应该使用异常处理机制。对于预料到可能发生的、可预防的错误情况,应该使用条件语句或返回特定值来处理,而不是依赖异常来控制流程。同时,在自定义异常类时,应该遵循Java异常类层次结构,并选择最合适的异常类型来表示特定的错误情况。

七、反射

反射是指能够在程序运行的时候动态的获取一个类的所有属性和方法,并且可以调用这些方法和属性。反射是框架的灵魂,之所以这样说是因为它赋予了我们在运行时分析类以及执行类中方法的能力。

Class

在Java中,Class类是一个非常重要的类,它是反射机制的核心之一。每个Java类在运行时都会有一个对应的Class对象,也被称为字节码对象,该对象包含了该类的运行时类型信息,包括类的构造函数、方法、字段、注解等。

使用反射的第一步就是要获取类的字节码对象,获取一个类的字节码对象通常有五种方法:

  • 方法一:通过一个类中的静态变量class获取

    Class<Person> personClass = Person.class;
    
  • 方法二:通过一个类的对象的getClass()方法获取

    Person person = new Person();
    Class<? extends Person> personClass = person.getClass();
    
  • 方法三:通过Class.forName()方法获取

    Class<?> forName = Class.forName("com.practice.reflection.Person");
    
  • 方法四:对于基本数据类型,直接通过.class获取

    Class<Integer> integerClass = int.class;
    
  • 方法五:对于基本类型的包装类,可以通过类中的静态变量TYPE获取Class类对象

    Class<Integer> integerClass = Integer.TYPE;
    

需要注意的一个类的字节码对象在整个JVM中只有一个,所以就算用不同的方式获取同一个类的字节码对象,拿到的都是同一个对象。

下面我们将用代码演示的方式来学习反射的具体应用,这里我们提供一个等会要使用反射操作的类:

public class Person {
    private Person(){

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

    private String name ;

    private Integer age;

    private List<String> hobbies;

    public void say() {
        System.out.println("我叫" + name + ",今年" + age + 
                "岁,我的爱好是:" + String.join("、", hobbies));
    }
    
    private void sleep() {
        System.out.println("睡觉是很私密的事");
    }
}
创建对象

使用反射创建对象,我们可以通过Class类提供的newInstance()方法

Class<Person> personClass = Person.class;
Person person = personClass.newInstance();

不过这个方法有个局限性,就是只能调用类的公共无参构造方法创建对象,如果要调用有参构造或者不是使用public访问修饰符修饰的构造方式是不行的,为了能够调用任意的构造方法,Java提供了Constructor类,我们可以通过这个类来调用任意的构造方法。

public static void main(String[] args) {
    Class<Person> personClass = Person.class;

    // 通过构造方法形参列表的类型来获取public的构造函数
    Constructor<Person> cons1 = personClass.getConstructor(String.class);

    // 获取所有public的构造函数
    Constructor<?>[] constructors = personClass.getConstructors();

    // 通过构造方法形参列表的类型来获取构造函数
    Constructor<Person> cons2 = personClass.getDeclaredConstructor();

    // 获取所有的构造函数
    Constructor<?>[] declaredConstructors = personClass.getDeclaredConstructors();

    // 通过Constructor调用public的构造函数
    Person person1 = cons1.newInstance("张三");

    // 通过Constructor调用私有的构造函数
    cons2.setAccessible(true); // 设置可访问私有的方法
    Person person2 = cons2.newInstance();
}
访问字段

对于任意对象的实例,只要我们能获取到它的Class对象,就能获取到它的一切信息,关于字段的信息Class类也提供了对应的方法

public static void main(String[] args) {
    Class<Person> personClass = Person.class;
    // 通过反射调用构造方法创建对象
    Person person = personClass.getConstructor(String.class).newInstance("张三");

    // 通过字段名称获取public的字段
    Field field = personClass.getField("name");

    // 获取所有public的字段
    Field[] fields = personClass.getFields();

    // 通过字段名称获取字段
    Field declaredField = personClass.getDeclaredField("age");

    // 获取所有字段
    Field[] declaredFields = personClass.getDeclaredFields();

    // 对指定对象的字段进行设置值
    declaredField.setAccessible(true); // 设置私有属性可访问
    declaredField.set(person, 18);

    // 通过反射获取字段的值
    Object o = declaredField.get(person);
    System.out.println(o); // 18
    System.out.println(person); // Person{name='张三', age=18, hobbies=null}
}
执行方法

话不多说,直接上代码

public static void main(String[] args)  {
    Class<Person> personClass = Person.class;

    Person person = personClass.getConstructor(String.class).newInstance("张三");

    // 通过名称和形参列表类型获取public的方法
    Method method = personClass.getMethod("say", Integer.class, List.class);

    // 获取所有public的方法
    Method[] methods = personClass.getMethods();

    // 通过名称和形参列表类型获取方法
    Method declaredMethod = personClass.getDeclaredMethod("sleep");

    // 获取所有方法
    Method[] declaredMethods = personClass.getDeclaredMethods();

    // 调用public的方法
    method.invoke(person, 18, Arrays.asList("写代码", "看电影", "打乒乓球")); // 我叫张三,今年18岁,我的爱好是:写代码、看电影、打乒乓球

    // 调用私有方法,还是要先设置可访问私有属性
    declaredMethod.setAccessible(true);
    declaredMethod.invoke(person); // 睡觉是很私密的事
}
获取注解、父类及接口

通过字节码对象除了获取本类相关的信息之外,还可以获取注解、父类和接口的信息

public static void main(String[] args) {
    Class<Person> personClass = Person.class;

    // 获取父类的字节码对象
    Class<? super Person> superclass = personClass.getSuperclass();

    // 获取实现的所有接口
    Class<?>[] interfaces = personClass.getInterfaces();

    // 获取所有的注解
    Annotation[] annotations = personClass.getAnnotations();
}

八、泛型

Java泛型(generics)是JDK1.5中引入的一个新特性,泛型是一种用于编写更加通用和灵活的代码的技术。它允许我们在编写类、接口和方法时使用参数化类型,这样我们可以在使用这些类、接口和方法时指定具体的类型。通过使用泛型,我们可以编写更加通用的代码,同时在编译时就能够进行类型检查,避免了在运行时出现类型错误的可能性。

在Java中,泛型使用尖括号(<>)来声明参数化类型,例如:List表示一个元素类型为String的列表。通过泛型,我们可以创建容器类、算法和数据结构,以及其他各种工具,这些都可以与不同的数据类型一起使用,从而提高了代码的重用性和可读性。

在Java中有三种泛型使用方式:

  • 泛型类
  • 泛型接口
  • 泛型方法
泛型类

泛型类是指把泛型定义在类上,语法如下,非常的简单

public class 类名 <泛型标识,...> {
    private 泛型标识 变量名;
    ...
}
  • 尖括<>中的泛型标识被称为参数类型,它可以代指任何的引用数据类型

  • 泛型标识顾名思义只是一个标识一个占位符,它可以写任意的值(如果你高兴写成Happy都行),但是一般来说会按照一个默认规范来写泛型标识

    T(Type): 代表一般的任何类
    E(Element): 代表元素,比如集合中的元素类型
    K(Key): 代表键,比如HashMap的key
    V(Value): 代表值,比如HashMap的value
    
  • 在泛型类中,类型参数定义的位置有三处,分别为:

    1.非静态的成员属性类型
    2.非静态方法的形参类型(包括非静态成员方法和构造器)
    3.非静态的成员方法的返回值类型
    

泛型类示例

public class GenericsTest <T> {
    // obj这个变量的类型为T,T的具体类型由外部传入
    private T obj;

    // 泛型作为方法的形参列表
    public void setObj(T t) {
        this.obj = t;
    }

    // 泛型作为方法的返回类型
    private T getObj() {
        return obj;
    }
}


public static void main(String[] args) {
    // 将泛型的具体类型传入,我这里传入的是String,那么会将GenericsTest中所有的T替换成String
    GenericsTest<String> genericsTest = new GenericsTest<>();

    genericsTest.setObj("Hello World!");
    // 输出Hello World!
    System.out.println(genericsTest.getObj());
}
泛型接口

泛型接口的定义语法跟泛型类差不多,具体语法如下:

public interface 接口名 <泛型标识,...> {
    ...
}

泛型接口示例:

public interface IGenerics <T, P> {
    // 方法的返回类型是泛型T,方法的参数类型是泛型P
    T testGenerics(P param);
}

需要注意的是泛型接口中的变量不能使用泛型,因为接口中的变量默认由public static final所修饰,属于静态变量,不能使用泛型类型。而接口中的泛型的具体类型需要在其实现类中指定。

// 实现IGenerics接口时指定泛型的具体类型
public class GenericsImpl implements IGenerics<String, Integer> {
    
    // 覆写的方法就会自动替换为具体的类型
    @Override
    public String testGenerics(Integer param) {
        return param.toString();
    }
}
泛型方法

泛型方法是指在方法上使用尖括号<>指定泛型,这个泛型类型的作用域仅在这个方法有效,并且可以适用于静态方法,其语法如下:

public [static] <泛型类型> 返回类型 方法名(类型参数 变量名) {
    ...
}

泛型方法示例:

public class GenericsTest <T> {

    // 使用类定义的泛型并不是反省放
    public T testGenerics(T t) {
        return t;
    }
    
    /**
     * 这里在方法上使用<>指定泛型类型才是泛型方法
     * 需要注意的是泛型方法定义的泛型和泛型类定义的泛型相互独立,它们之间没有任何关系
     */
	public <P> T testGenericsMethod(P param, T t) {
        return t;
    }
}
泛型通配符

在现实编码中,有时候我们需要泛型能够处理某一个类型范围的类型参数,比如某个泛型类和他的子类,为此Java引入了反省通配符这个概念,泛型通配符有三种形式:

  • <?>:被称作无限定的通配符,代表任意数据类型
  • <? extends T>:被称作有上界的通配符,其只能接收T及其子类的泛型
  • <? super T>:被称作有上界的通配符,其只能接受T及其父类,直至Object

泛型通配符示例:

public class GenericsTest {

    /**
     * 无限定的通配符
     */
    public static void printValOne(List<?> list) {
        for (Object o : list) {
            System.out.println(o);
        }
    }

    /**
     * 有上界的通配符,表示只能接收Number及其子类
     */
    public static void printValTow(List<? extends Number> list) {
        for (Number number : list) {
            System.out.println(number);
        }
    }

    /**
     * 有下界的通配符,表示只能接受Number及其父类
     */
    public static void printValThree(List<? super Number> list) {
        for (Object o : list) {
            System.out.println(o);
        }
    }


    public static void main(String[] args) {
        List<String> strList = new ArrayList<>(Arrays.asList("aaa", "bbb"));
        List<Integer> intList = new ArrayList<>(Arrays.asList(1, 2));
        List<Number> numList = new ArrayList<>(Arrays.asList(4, 5));
        List<Object> objList = new ArrayList<>(Arrays.asList("obj", 7L, 8D));

        // 无限定通配符接收String类型的列表
        printValOne(strList);
        // 无限定通配符接收Integer类型的列表
        printValOne(intList);

        // 有上界通配符接收Integer类型的列表,因为Integer是Number的子类所以可以接收
        printValTow(intList);
        // 有上界通配符接收Number类型的列表,如果接收的是非Number及其子类会编译报错
        printValTow(numList);

        // 有下届通配符接收Number类型的列表,如果接收的是非Number及其父类会编译报错
        printValThree(numList);
        // 有下届通配符接收Object类型的列表,因为Object是Number类的父类,所以可以接收
        printValThree(objList);
    }
}

// 其运行结果为:
// aaa
// bbb
// 1
// 2
// 1
// 2
// 4
// 5
// 4
// 5
// obj
// 7
// 8.0
类型擦除

泛型的本质是将类型参数化,他是通过类型擦除的方式来实现的,即编译器会将泛型类型的信息在编译时擦除,并且在生成的字节码中不包含泛型类型的参数信息。这是为了向后兼容旧版Java代码,因为泛型是在JDK 5中引入的新特性,为了与之前的版本兼容,Java使用了类型擦除来实现泛型。

换而言之泛型信息只会存在于编译阶段,在代码编译结束后与泛型相关的信息将会被擦掉,并不会进入到运行时期,这就是类型擦除

举例说明:

public static void main(String[] args) {
    List<String> strList = new ArrayList<String>();
    List<Integer> intList = new ArrayList<Integer>();
    System.out.println(strList.getClass() == intList.getClass()); // true
}

并且如果在idea中试图使用不同的泛型进行方法重载会编译报错,idea会提示两个方法有相同的类型擦除

在这里插入图片描述

那么到这里大家可能会产生一个疑问,不是说泛型信息在编译的时候就会被擦除掉吗?那既然泛型信息被擦除了,如何保证我们在集合中只添加指定的数据类型的对象呢?比如说我们虽然定义了List< Integer > 泛型集合,但其泛型信息最终被擦除后就变成了 List< Object > 集合,那为什么不允许向其中插入 String 对象呢?

其实在创建一个泛型类的对象时, Java 编译器是先检查代码中传入 < T > 的数据类型,并记录下来,然后再对代码进行编译,编译的同时进行类型擦除;如果需要对被擦除了泛型信息的对象进行操作,编译器会自动将对象进行类型转换。

擦除List< Integer > 的泛型信息后,get() 方法的返回值将返回 Object 类型,但编译器会自动插入 Integer 的强制类型转换。也就是说,编译器把 get() 方法调用翻译为两条字节码指令:

  • 对原始方法 get() 的调用,返回的是 Object 类型;
  • 将返回的 Object 类型强制转换为 Integer 类型;

代码如下:

Integer n = arrayInteger.get(0); // 这条代码底层如下:

//(1)get() 方法的返回值返回的是 Object 类型
Object object = arrayInteger.get(0);

//(2)编译器自动插入 Integer 的强制类型转换
Integer n = (Integer) object;

注解

Java注解(Annotation)也被称为Java标注,是JDK1.5引入的一种注释机制。但是注解和Java注释并不是同一种东西,Java注释偏向的是解释说明,并不会被编译,而注解偏向的是标注,在Java 中的类、方法、变量、参数和包等都可以被标注,Java 标注可以通过反射获取标注内容。在编译器生成类文件时,标注可以被嵌入到字节码中。Java 虚拟机可以保留标注内容,在运行时可以获取到标注内容 。 当然它也支持自定义 Java 标注。

Java的内置注解

Java 定义了一套注解,共有 7 个,3 个在 java.lang 中,剩下 4 个在 java.lang.annotation 中。

作用在代码的注解是:

  • @Override:检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
  • @Deprecated :标记过时方法。如果使用该方法,会报编译警告。
  • @SuppressWarnings:指示编译器去忽略注解中声明的警告。

作用在其他注解的注解(或者说 元注解)是:

  • @Retention:标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。
  • @Documented:标记这些注解是否包含在用户文档中。
  • @Target:标记这个注解应该是哪种 Java 成员。
  • @Inherited:标记这个注解是继承于哪个注解类(默认注解并没有继承于任何子类)

从 Java 7 开始,额外添加了 3 个注解:

  • @SafeVarargs:Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
  • @FunctionalInterface:Java 8 开始支持,标识一个匿名函数或函数式接口。
  • @Repeatable:Java 8 开始支持,标识某注解可以在同一个声明上使用多次。
自定义注解

在Java中自定义注解非常简单,使用@interface来定义注解,然后在注解上加上元注解进行修饰即可

/**
 * 使用@interface定义注解
 *
 * 元注解的具体含义以及取值:
 *  @Documented:用于指定被该注解注解的类的注解将被javadoc工具提取成文档。
 *
 *  @Inherited:用于指定被该注解注解的类的子类是否继承该注解。
 *
 *  @Target:指定注解可以应用的程序元素类型。取值包括:
 *      ElementType.TYPE:可以应用于类、接口、枚举
 *      ElementType.FIELD:可以应用于字段(成员变量)
 *      ElementType.METHOD:可以应用于方法
 *      ElementType.PARAMETER:可以应用于方法的参数
 *      ElementType.CONSTRUCTOR:可以应用于构造方法
 *      ElementType.LOCAL_VARIABLE:可以应用于局部变量
 *      ElementType.ANNOTATION_TYPE:可以应用于注解
 *      ElementType.PACKAGE:可以应用于包
 *
 *  @Retention:指定注解的生命周期。取值包括:
 *      RetentionPolicy.SOURCE:在源代码中有效,编译时丢弃
 *      RetentionPolicy.CLASS:在编译时有效,运行时丢弃
 *      RetentionPolicy.RUNTIME:在运行时有效,可以通过反射获取
 */
@Documented
@Inherited
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
    // 注解还可以拥有元素元素定义语法为: 数据类型 元素名称() [default 默认值];

    // 定义一个类型为String名为name的注解元素,默认值为test
    String name() default "test";
    
    // 定义一个类型为String数组的value元素,没有默认值
    String[] value();
}
类型;

代码如下:

```java
Integer n = arrayInteger.get(0); // 这条代码底层如下:

//(1)get() 方法的返回值返回的是 Object 类型
Object object = arrayInteger.get(0);

//(2)编译器自动插入 Integer 的强制类型转换
Integer n = (Integer) object;

注解

Java注解(Annotation)也被称为Java标注,是JDK1.5引入的一种注释机制。但是注解和Java注释并不是同一种东西,Java注释偏向的是解释说明,并不会被编译,而注解偏向的是标注,在Java 中的类、方法、变量、参数和包等都可以被标注,Java 标注可以通过反射获取标注内容。在编译器生成类文件时,标注可以被嵌入到字节码中。Java 虚拟机可以保留标注内容,在运行时可以获取到标注内容 。 当然它也支持自定义 Java 标注。

Java的内置注解

Java 定义了一套注解,共有 7 个,3 个在 java.lang 中,剩下 4 个在 java.lang.annotation 中。

作用在代码的注解是:

  • @Override:检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
  • @Deprecated :标记过时方法。如果使用该方法,会报编译警告。
  • @SuppressWarnings:指示编译器去忽略注解中声明的警告。

作用在其他注解的注解(或者说 元注解)是:

  • @Retention:标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。
  • @Documented:标记这些注解是否包含在用户文档中。
  • @Target:标记这个注解应该是哪种 Java 成员。
  • @Inherited:标记这个注解是继承于哪个注解类(默认注解并没有继承于任何子类)

从 Java 7 开始,额外添加了 3 个注解:

  • @SafeVarargs:Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
  • @FunctionalInterface:Java 8 开始支持,标识一个匿名函数或函数式接口。
  • @Repeatable:Java 8 开始支持,标识某注解可以在同一个声明上使用多次。
自定义注解

在Java中自定义注解非常简单,使用@interface来定义注解,然后在注解上加上元注解进行修饰即可

/**
 * 使用@interface定义注解
 *
 * 元注解的具体含义以及取值:
 *  @Documented:用于指定被该注解注解的类的注解将被javadoc工具提取成文档。
 *
 *  @Inherited:用于指定被该注解注解的类的子类是否继承该注解。
 *
 *  @Target:指定注解可以应用的程序元素类型。取值包括:
 *      ElementType.TYPE:可以应用于类、接口、枚举
 *      ElementType.FIELD:可以应用于字段(成员变量)
 *      ElementType.METHOD:可以应用于方法
 *      ElementType.PARAMETER:可以应用于方法的参数
 *      ElementType.CONSTRUCTOR:可以应用于构造方法
 *      ElementType.LOCAL_VARIABLE:可以应用于局部变量
 *      ElementType.ANNOTATION_TYPE:可以应用于注解
 *      ElementType.PACKAGE:可以应用于包
 *
 *  @Retention:指定注解的生命周期。取值包括:
 *      RetentionPolicy.SOURCE:在源代码中有效,编译时丢弃
 *      RetentionPolicy.CLASS:在编译时有效,运行时丢弃
 *      RetentionPolicy.RUNTIME:在运行时有效,可以通过反射获取
 */
@Documented
@Inherited
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
    // 注解还可以拥有元素元素定义语法为: 数据类型 元素名称() [default 默认值];

    // 定义一个类型为String名为name的注解元素,默认值为test
    String name() default "test";
    
    // 定义一个类型为String数组的value元素,没有默认值
    String[] value();
}
  • 28
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值