这篇 Java 基础,我吹不动了(五)

简介: Hey guys,这里是程序员cxuan,欢迎你收看我最新一期的文章,这篇文章我补充了一些关于《Java基础核心总结》的内容,修改了部分错别字和语句不通顺的地方,并且对内部类、泛型等内容进行了一定的补充,并且我在文章有些地方给出了一些链接,这些链接都是我自己写的硬核文章,能够更好的帮助你理解 Java 这门语言,那么废话不多说,下面开始正文。

HashMap

HashMap 是一个利用哈希表原理来存储元素的集合,并且允许空的 key-value 键值对。HashMap 是非线程安全的,也就是说在多线程的环境下,可能会存在问题,而 Hashtable 是线程安全的容器。HashMap 也支持 fail-fast 机制。HashMap 的实例有两个参数影响其性能:初始容量 和加载因子。可以使用 Collections.synchronizedMap(new HashMap(...)) 来构造一个线程安全的 HashMap。

TreeMap 类

一个基于 NavigableMap 实现的红黑树。这个 map 根据 key 自然排序存储,或者通过 Comparator 进行定制排序。

  • TreeMap 为 containsKey,get,put 和remove方法提供了 log(n) 的时间开销。
  • 注意这个实现不是线程安全的。如果多线程并发访问 TreeMap,并且至少一个线程修改了 map,必须进行外部加锁。这通常通过在自然封装集合的某个对象上进行同步来实现,或者使用 SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...))
  • 这个实现持有fail-fast机制。

LinkedHashMap 类

LinkedHashMap 是 Map 接口的哈希表和链表的实现。这个实现与 HashMap 不同之处在于它维护了一个贯穿其所有条目的双向链表。这个链表定义了遍历顺序,通常是插入 map 中的顺序。

  • 它提供一个特殊的 LinkedHashMap(int,float,boolean) 构造器来创建 LinkedHashMap,其遍历顺序是其最后一次访问的顺序。
  • 可以重写 removeEldestEntry(Map.Entry) 方法,以便在将新映射添加到 map 时强制删除过期映射的策略。
  • 这个类提供了所有可选择的 map 操作,并且允许 null 元素。由于维护链表的额外开销,性能可能会低于HashMap,有一条除外:遍历 LinkedHashMap 中的 collection-views 需要与 map.size 成正比,无论其容量如何。HashMap 的迭代看起来开销更大,因为还要求时间与其容量成正比。
  • LinkedHashMap 有两个因素影响了它的构成:初始容量和加载因子。
  • 注意这个实现不是线程安全的。如果多线程并发访问LinkedHashMap,并且至少一个线程修改了map,必须进行外部加锁。这通常通过在自然封装集合的某个对象上进行同步来实现 Map m = Collections.synchronizedMap(new LinkedHashMap(...))
  • 这个实现持有fail-fast机制。

Hashtable 类

Hashtable 类实现了一个哈希表,能够将键映射到值。任何非空对象都可以用作键或值。

  • 此实现类支持 fail-fast 机制
  • 与新的集合实现不同,Hashtable 是线程安全的。如果不需要线程安全的容器,推荐使用 HashMap,如果需要多线程高并发,推荐使用 ConcurrentHashMap

IdentityHashMap 类

IdentityHashMap 是比较小众的 Map 实现了。

  • 这个类不是一个通用的 Map 实现!虽然这个类实现了 Map 接口,但它故意违反了 Map 的约定,该约定要求在比较对象时使用 equals 方法,此类仅适用于需要引用相等语义的极少数情况。
  • 同 HashMap,IdentityHashMap 也是无序的,并且该类不是线程安全的,如果要使之线程安全,可以调用Collections.synchronizedMap(new IdentityHashMap(...))方法来实现。
  • 支持 fail-fast 机制

WeakHashMap 类

WeakHashMap 类基于哈希表的 Map 基础实现,带有弱键。WeakHashMap 中的 entry 当不再使用时还会自动移除。更准确的说,给定key的映射的存在将不会阻止 key 被垃圾收集器丢弃。

  • 基于 map 接口,是一种弱键相连,WeakHashMap 里面的键会自动回收
  • 支持 null 值和 null 键。
  • fast-fail 机制
  • 不允许重复
  • WeakHashMap 经常用作缓存

Collections 类

Collections 不属于 Java 框架继承树上的内容,它属于单独的分支,Collections 是一个包装类,它的作用就是为集合框架提供某些功能实现,此类只包括静态方法操作或者返回 collections。

同步包装

同步包装器将自动同步(线程安全性)添加到任意集合。六个核心集合接口(Collection,Set,List,Map,SortedSet 和 SortedMap)中的每一个都有一个静态工厂方法。

public static  Collection synchronizedCollection(Collection c);
public static  Set synchronizedSet(Set s);
public static  List synchronizedList(List list);
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m);
public static  SortedSet synchronizedSortedSet(SortedSet s);
public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m);

不可修改的包装

不可修改的包装器通过拦截修改集合的操作并抛出 UnsupportedOperationException,主要用在下面两个情景:

  • 构建集合后使其不可变。在这种情况下,最好不要去获取返回 collection 的引用,这样有利于保证不变性
  • 允许某些客户端以只读方式访问你的数据结构。你保留对返回的 collection 的引用,但分发对包装器的引用。通过这种方式,客户可以查看但不能修改,同时保持完全访问权限。

这些方法是:

public static  Collection unmodifiableCollection(Collection<? extends T> c);
public static  Set unmodifiableSet(Set<? extends T> s);
public static  List unmodifiableList(List<? extends T> list);
public static <K,V> Map<K, V> unmodifiableMap(Map<? extends K, ? extends V> m);
public static  SortedSet unmodifiableSortedSet(SortedSet<? extends T> s);
public static <K,V> SortedMap<K, V> unmodifiableSortedMap(SortedMap<K, ? extends V> m);

线程安全的Collections

Java1.5 并发包 (java.util.concurrent) 提供了线程安全的 collections 允许遍历的时候进行修改,通过设计iterator 为 fail-fast 并抛出 ConcurrentModificationException。一些实现类是CopyOnWriteArrayListConcurrentHashMapCopyOnWriteArraySet

Collections 算法

此类包含用于集合框架算法的方法,例如二进制搜索,排序,重排,反向等。

集合实现类特征图

下图汇总了部分集合框架的主要实现类的特征图,让你能有清晰明了看出每个实现类之间的差异性

微信图片_20220417151327.jpg

泛型

在 Jdk1.5 中,提出了一种新的概念:泛型,那么什么是泛型呢?

泛型其实就是一种参数化的集合,它限制了你添加进集合的类型。泛型的本质就是一种参数化类型。多态也可以看作是泛型的机制。一个类继承了父类,那么就能通过它的父类找到对应的子类,但是不能通过其他类来找到具体要找的这个类。泛型的设计之处就是希望对象或方法具有最广泛的表达能力。

下面来看一个例子说明没有泛型的用法

List arrayList = new ArrayList();
arrayList.add("cxuan");
arrayList.add(100);
for(int i = 0; i< arrayList.size();i++){
    String item = (String)arrayList.get(i);
  System.out.println("test === ", item);
}

这段程序不能正常运行,原因是 Integer  类型不能直接强制转换为 String 类型

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

如果我们用泛型进行改写后,示例代码如下

List<String> arrayList = new ArrayList<String>();
arrayList.add(100);

这段代码在编译期间就会报错,编译器会在编译阶段就能够帮我们发现类似这样的问题。

泛型的使用

泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法,下面我们就来一起探讨一下。

用泛型表示类

泛型可以加到类上面,来表示这个类的类型

//此处 T 可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
public class GenericDemo<T>{ 
    //value 这个成员变量的类型为T,T的类型由外部指定  
    private T value;
    public GenericDemo(T value) {
        this.value = value;
    }
    public T getValue(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
        return value;
    }
   public void setValue(T value){
       this.value = value
    }
}

用泛型表示接口

泛型接口与泛型类的定义及使用基本相同。

//定义一个泛型接口
public interface Generator<T> {
    public T next();
}

一般泛型接口常用于 生成器(generator) 中,生成器相当于对象工厂,是一种专门用来创建对象的类。

泛型方法

可以使用泛型来表示方法

public class GenericMethods {
  public <T> void f(T x){
    System.out.println(x.getClass().getName());
  }
}

泛型通配符

无限制通配符<?>

List 是泛型类,为了 表示各种泛型 List 的父类,可以使用类型通配符,类型通配符使用问号(?)表示,它的元素类型可以匹配任何类型。例如

public static void main(String[] args) {
    List<String> name = new ArrayList<String>();
    List<Integer> age = new ArrayList<Integer>();
    List<Number> number = new ArrayList<Number>();
    name.add("cxuan");
    age.add(18);
    number.add(314);
    generic(name);
    generic(age);
    generic(number);   
}
public static void generic(List<?> data) {
    System.out.println("Test cxuan :" + data.get(0));
}

上界通配符

在类型参数中使用 extends 表示这个泛型中的参数必须是 E 或者 E 的子类,这样有两个好处:

  • 如果传入的类型不是 E 或者 E 的子类,编辑不成功
  • 泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用

举个例子:

private <K extends ChildBookBean, E extends BookBean> E test2(K arg1, E arg2){
    E result = arg2;
    arg2.compareTo(arg1);
    //.....
    return result;
}

下界通配符

在类型参数中使用 super 表示这个泛型中的参数必须是 E 或者 E 的父类。

private <E> void add(List<? super E> dst, List<E> src){
    for (E e : src) {
        dst.add(e);
    }
}

可以看到,上面的 dst 类型 “大于等于” src 的类型,这里的“大于等于”是指 dst 表示的范围比 src 要大,因此装得下 dst 的容器也就能装 src。

通配符比较

通过上面的例子我们可以知道,无限制通配符 < ?> 和 Object 有些相似,用于表示无限制或者不确定范围的场景。

两种有限制通配形式 < ? super E> 和 < ? extends E> 也比较容易混淆,我们再来比较下。

它们的目的都是为了使方法接口更为灵活,可以接受更为广泛的类型。

< ? super E> 用于灵活写入或比较,使得对象可以写入父类型的容器,使得父类型的比较方法可以应用于子类对象。< ? extends E> 用于灵活读取,使得方法可以读取 E 或 E 的任意子类型的容器对象。

泛型的类型擦除

Java 中的泛型和 C++ 中的模板有一个很大的不同:

C++ 模板的实例化会为每一种类型都产生一套不同的代码,这就是所谓的代码膨胀。Java 并不会产生这个问题。虚拟机中并没有泛型类型对象,所有的对象都是普通类。(摘自:http://blog.csdn.net/fw0124/article/details/42295463

在 Java 中,泛型是 Java 编译器的概念,用泛型编写的 Java 程序和普通的 Java 程序基本相同,只是多了一些参数化的类型同时少了一些类型转换。

实际上泛型程序也是首先被转化成一般的、不带泛型的 Java 程序后再进行处理的,编译器自动完成了从 Generic Java 到普通 Java 的翻译,Java 虚拟机运行时对泛型基本一无所知。

当编译器对带有泛型的 Java 代码进行编译时,它会去执行类型检查和类型推断,然后生成普通的不带泛型的字节码,这种普通的字节码可以被一般的 Java 虚拟机接收并执行,这在就叫做 类型擦除(type erasure)。

实际上无论你是否使用泛型,集合框架中存放对象的数据类型都是 Object,这一点不仅仅从源码中可以看到,通过反射也可以看到。

List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
System.out.println(strings.getClass() == integers.getClass());//true

上面代码输出结果并不是预期的 false,而是 true。其原因就是泛型的擦除。

反射

反射是 Java 中一个非常重要同时也是一个高级特性,基本上 Spring 等一系列框架都是基于反射的思想写成的。我们首先来认识一下什么反射。

Java 反射机制是在程序的运行过程中,对于任何一个类,都能够知道它的所有属性和方法;对于任意一个对象,都能够知道调用它的任意属性和方法,这种动态获取信息以及动态调用对象方法的功能称为 Java 语言的反射机制

要想解剖一个类,必须先要获取到该类的字节码文件对象。而解剖使用的就是 Class 类中的方法,所以先要获取到每一个字节码文件对应的 Class 类型的对象.

所谓反射其实是获取类的字节码文件,也就是. class 文件,那么我们就可以通过 Class 这个对象进行获取。

Java 反射机制主要提供了以下这几个功能

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

这么一看,反射就像是一个掌控全局的角色,不管你程序怎么运行,我都能够知道你这个类有哪些属性和方法,你这个对象是由谁调用的,嗯,很屌。

在 Java 中,使用 Java.lang.reflect包实现了反射机制。Java.lang.reflect 所设计的类如下

微信图片_20220417151336.jpg

下面是一个简单的反射类

public class Person {
    public String name;// 姓名
    public int age;// 年龄
    public Person() {
        super();
    }
    public Person(String name, int age) {
        super();
        this.name = name;
        this.age = age;
    }
    public String showInfo() {
        return "name=" + name + ", age=" + age;
    }
}
public class Student extends Person implements Study {
    public String className;// 班级
    private String address;// 住址
    public Student() {
        super();
    }
    public Student(String name, int age, String className, String address) {
        super(name, age);
        this.className = className;
        this.address = address;
    }
    public Student(String className) {
        this.className = className;
    }
    public String toString() {
        return "姓名:" + name + ",年龄:" + age + ",班级:" + className + ",住址:"
                + address;
    }
    public String getAddress() {
        return address;
    }
    public void setAddress(String address) {
        this.address = address;
    }
}
public class TestRelect {
    public static void main(String[] args) {
        Class student = null;
        try {
            student = Class.forName("com.cxuan.reflection.Student");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        // 获取对象的所有公有属性。
        Field[] fields = student.getFields();
        for (Field f : fields) {
            System.out.println(f);
        }
        System.out.println("---------------------");
        // 获取对象所有属性,但不包含继承的。
        Field[] declaredFields = student.getDeclaredFields();
        for (Field df : declaredFields) {
            System.out.println(df);
        }
       // 获取对象的所有公共方法
        Method[] methods = student.getMethods();
        for (Method m : methods) {
            System.out.println(m);
        }
        System.out.println("---------------------");
        // 获取对象所有方法,但不包含继承的
        Method[] declaredMethods = student.getDeclaredMethods();
        for (Method dm : declaredMethods) {
            System.out.println(dm);
        }
       // 获取对象所有的公共构造方法
        Constructor[] constructors = student.getConstructors();
        for (Constructor c : constructors) {
            System.out.println(c);
        }
        System.out.println("---------------------");
        // 获取对象所有的构造方法
        Constructor[] declaredConstructors = student.getDeclaredConstructors();
        for (Constructor dc : declaredConstructors) {
            System.out.println(dc);
        }
       Class c = Class.forName("com.cxuan.reflection.Student");
       Student stu1 = (Student) c.newInstance();
       // 第一种方法,实例化默认构造方法,调用set赋值
        stu1.setAddress("河北石家庄");
        System.out.println(stu1);
        // 第二种方法 取得全部的构造函数 使用构造函数赋值
        Constructor<Student> constructor = c.getConstructor(String.class, 
                                                            int.class, String.class, String.class);
        Student student2 = (Student) constructor.newInstance("cxuan", 24, "六班", "石家庄");
        System.out.println(student2);
        /**
        * 獲取方法并执行方法
        */
        Method show = c.getMethod("showInfo");//获取showInfo()方法
        Object object = show.invoke(stu2);//调用showInfo()方法
    }
}

有一些是比较常用的,有一些是我至今都没见过怎么用的,下面进行一个归类。

与 Java 反射有关的类主要有

Class 类

在 Java 中,你每定义一个 java class 实体都会产生一个 Class 对象。也就是说,当我们编写一个类,编译完成后,在生成的 .class 文件中,就会产生一个 Class 对象,这个 Class 对象用于表示这个类的类型信息。Class 中没有公共的构造器,也就是说 Class 对象不能被实例化。下面来简单看一下 Class 类都包括了哪些方法

toString()

public String toString() {
  return (isInterface() ? "interface " : (isPrimitive() ? "" : "class "))
    + getName();
}

toString() 方法能够将对象转换为字符串,toString() 首先会判断 Class 类型是否是接口类型,也就是说,普通类和接口都能够用 Class 对象来表示,然后再判断是否是基本数据类型,这里判断的都是基本数据类型和包装类,还有 void类型。

所有的类型如下

  • java.lang.Boolean : 代表 boolean 数据类型的包装类
  • java.lang.Character: 代表 char 数据类型的包装类
  • java.lang.Byte: 代表 byte 数据类型的包装类
  • java.lang.Short: 代表 short 数据类型的包装类
  • java.lang.Integer: 代表 int 数据类型的包装类
  • java.lang.Long: 代表 long 数据类型的包装类
  • java.lang.Float: 代表 float 数据类型的包装类
  • java.lang.Double: 代表 double 数据类型的包装类
  • java.lang.Void: 代表 void 数据类型的包装类

然后是 getName() 方法,这个方法返回类的全限定名称。

  • 如果是引用类型,比如 String.class.getName()  -> java.lang.String
  • 如果是基本数据类型,byte.class.getName() -> byte
  • 如果是数组类型,new Object[3]).getClass().getName() -> [Ljava.lang.Object

toGenericString()

这个方法会返回类的全限定名称,而且包括类的修饰符和类型参数信息。

forName()

根据类名获得一个 Class 对象的引用,这个方法会使类对象进行初始化。

例如 Class t = Class.forName("java.lang.Thread") 就能够初始化一个 Thread 线程对象

在 Java 中,一共有三种获取类实例的方式

  • Class.forName(java.lang.Thread)
  • Thread.class
  • thread.getClass()

newInstance()

创建一个类的实例,代表着这个类的对象。上面 forName() 方法对类进行初始化,newInstance 方法对类进行实例化。

getClassLoader()

获取类加载器对象。

getTypeParameters()

按照声明的顺序获取对象的参数类型信息。

getPackage()

返回类的包

getInterfaces()

获得当前类实现的类或是接口,可能是有多个,所以返回的是 Class 数组。

Cast

把对象转换成代表类或是接口的对象

asSubclass(Class clazz)

把传递的类的对象转换成代表其子类的对象

getClasses()

返回一个数组,数组中包含该类中所有公共类和接口类的对象

getDeclaredClasses()

返回一个数组,数组中包含该类中所有类和接口类的对象

getSimpleName()

获得类的名字

getFields()

获得所有公有的属性对象

getField(String name)

获得某个公有的属性对象

getDeclaredField(String name)

获得某个属性对象

getDeclaredFields()

获得所有属性对象

getAnnotation(Class annotationClass)

返回该类中与参数类型匹配的公有注解对象

getAnnotations()

返回该类所有的公有注解对象

getDeclaredAnnotation(Class annotationClass)

返回该类中与参数类型匹配的所有注解对象

getDeclaredAnnotations()

返回该类所有的注解对象

getConstructor(Class...<?> parameterTypes)

获得该类中与参数类型匹配的公有构造方法

getConstructors()

获得该类的所有公有构造方法

getDeclaredConstructor(Class...<?> parameterTypes)

获得该类中与参数类型匹配的构造方法

getDeclaredConstructors()

获得该类所有构造方法

getMethod(String name, Class...<?> parameterTypes)

获得该类某个公有的方法

getMethods()

获得该类所有公有的方法

getDeclaredMethod(String name, Class...<?> parameterTypes)

获得该类某个方法

getDeclaredMethods()

获得该类所有方法

Field 类

Field 类提供类或接口中单独字段的信息,以及对单独字段的动态访问。

这里就不再对具体的方法进行介绍了,读者有兴趣可以参考官方 API

这里只介绍几个常用的方法

equals(Object obj)

属性与obj相等则返回true

get(Object obj)

获得obj中对应的属性值

set(Object obj, Object value)

设置obj中对应属性值

Method 类

invoke(Object obj, Object... args)

传递object对象及参数调用该对象对应的方法

ClassLoader 类

反射中,还有一个非常重要的类就是 ClassLoader 类,类装载器是用来把类(class) 装载进 JVM的。ClassLoader 使用的是双亲委托模型来搜索加载类的,这个模型也就是双亲委派模型。ClassLoader 的类继承图如下

微信图片_20220417151346.jpg

深入理解反射,可以阅读作者的这篇文章 学会反射后,我被录取了!(干货)

枚举

枚举可能是我们使用次数比较少的特性,在 Java 中,枚举使用 enum 关键字来表示,枚举其实是一项非常有用的特性,你可以把它理解为具有特定性质的类。enum 不仅仅 Java 有,C 和 C++ 也有枚举的概念。下面是一个枚举的例子。

public enum Family {
    FATHER,
    MOTHER,
    SON,
    Daughter;
}

上面我们创建了一个 Family的枚举类,它具有 4 个值,由于枚举类型都是常量,所以都用大写字母来表示。那么 enum 创建出来了,该如何引用呢?

public class EnumUse {
    public static void main(String[] args) {
        Family s = Family.FATHER;
    }
}

枚举特性

enum 枚举这个类比较有意思,当你创建完 enum 后,编译器会自动为你的 enum 添加 toString() 方法,能够让你方便的显示 enum 实例的具体名字是什么。除了 toString() 方法外,编译器还会添加 ordinal() 方法,这个方法用来表示 enum 常量的声明顺序,以及 values() 方法显示顺序的值。

public static void main(String[] args) {
  for(Family family : Family.values()){
    System.out.println(family + ", ordinal" + family.ordinal());
  }
}

enum 可以进行静态导入包,静态导入包可以做到不用输入 枚举类名.常量,可以直接使用常量,神奇吗? 使用 ennum 和 static 关键字可以做到静态导入包

微信图片_20220417151350.jpg

上面代码导入的是 Family 中所有的常量,也可以单独指定常量。

枚举和普通类一样

枚举就和普通类一样,除了枚举中能够方便快捷的定义常量,我们日常开发使用的 public static final xxx 其实都可以用枚举来定义。在枚举中也能够定义属性和方法,千万不要把它看作是异类,它和万千的类一样。

public enum OrdinalEnum {
    WEST("live in west"),
    EAST("live in east"),
    SOUTH("live in south"),
    NORTH("live in north");
    String description;
    OrdinalEnum(String description){
        this.description = description;
    }
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
    public static void main(String[] args) {
        for(OrdinalEnum ordinalEnum : OrdinalEnum.values()){
            System.out.println(ordinalEnum.getDescription());
        }
    }
}

一般 switch 可以和 enum 一起连用,来构造一个小型的状态转换机。

enum Signal {
  GREEN, YELLOW, RED
}
public class TrafficLight {
    Signal color = Signal.RED;
    public void change() {
        switch (color) {
        case RED:
            color = Signal.GREEN;
            break;
        case YELLOW:
            color = Signal.RED;
            break;
        case GREEN:
            color = Signal.YELLOW;
            break;
        }
    }
}

是不是代码顿时觉得优雅整洁了些许呢?

枚举神秘之处

在 Java 中,万事万物都是对象,enum 虽然是个关键字,但是它却隐式的继承于 Enum 类。我们来看一下 Enum 类,此类位于 java.lang 包下,可以自动引用。

微信图片_20220417151355.jpg

此类的属性和方法都比较少。你会发现这个类中没有我们的 values 方法。前面刚说到,values() 方法是你使用枚举时被编译器添加进来的 static 方法。可以使用反射来验证一下

除此之外,enum 还和 Class 类有交集,在 Class 类中有三个关于 Enum 的方法

微信图片_20220417151359.jpg

前面两个方法用于获取 enum 常量,isEnum 用于判断是否是枚举类型的。

枚举类

除了 Enum 外,还需要知道两个关于枚举的工具类,一个是 EnumSet ,一个是 EnumMap

EnumSet 和 EnumMap

EnumSet 是 JDK1.5 引入的,EnumSet 的设计充分考虑到了速度因素,使用 EnumSet 可以作为 Enum 的替代者,因为它的效率比较高。

EnumMap 是一种特殊的 Map,它要求其中的 key 键值是来自一个 enum。因为 EnumMap 速度也很快,我们可以使用 EnumMap 作为 key 的快速查找。

总的来说,枚举的使用不是很复杂,它也是 Java 中很小的一块功能,但有时却能够因为这一个小技巧,能够让你的代码变得优雅和整洁。

I/O

创建一个良好的 I/O 程序是非常复杂的。JDK 开发人员编写了大量的类只为了能够创建一个良好的工具包,想必编写 I/O 工具包很费劲吧。

IO 类设计出来,肯定是为了解决 IO 相关操作的,最常见的 I/O 读写就是网络、磁盘等。在 Java 中,对文件的操作是一个典型的 I/O 操作。下面我们就对 I/O 进行一个分类。

微信图片_20220417151403.jpg

I/O 还可以根据操作对象来进行区分:主要分为

微信图片_20220417151407.jpg

除此之外,I/O 中还有其他比较重要的类

File 类

File 类是对文件系统中文件以及文件夹进行操作的类,可以通过面向对象的思想操作文件和文件夹,是不是很神奇?

文件创建操作如下,主要涉及 文件创建、删除文件、获取文件描述符等

class FileDemo{
   public static void main(String[] args) {
       File file = new File("D:\\file.txt");
       try{
         f.createNewFile(); // 创建一个文件
         // File类的两个常量
         //路径分隔符(与系统有关的)<windows里面是 ; linux里面是 : >
        System.out.println(File.pathSeparator);  //   ;
        //与系统有关的路径名称分隔符<windows里面是 \ linux里面是/ >
        System.out.println(File.separator);      //  \
         // 删除文件
         /*
         File file = new File(fileName);
         if(f.exists()){
             f.delete();
         }else{
             System.out.println("文件不存在");
         }   
         */
       }catch (Exception e) {
           e.printStackTrace();
       }
    }
}

也可以对文件夹进行操作

class FileDemo{
  public static void main(String[] args) {
    String fileName = "D:"+ File.separator + "filepackage";
    File file = new File(fileName);
    f.mkdir();
  // 列出所有文件
    /*
    String[] str = file.list();
    for (int i = 0; i < str.length; i++) {
      System.out.println(str[i]);
    }
    */
    // 使用 file.listFiles(); 列出所有文件,包括隐藏文件
    // 使用 file.isDirectory() 判断指定路径是否是目录
  }
}

上面只是举出来了两个简单的示例,实际上,还有一些其他对文件的操作没有使用。比如创建文件,就可以使用三种方式来创建

File(String directoryPath);
File(String directoryPath, String filename);
File(File dirObj, String filename);

directoryPath 是文件的路径名,filename 是文件名,dirObj 是一个 File 对象。例如

File file = new File("D:\\java\\file1.txt");  //双\\是转义
System.out.println(file);
File file2 = new File("D:\\java","file2.txt");//父路径、子路径--可以适用于多个文件的!
System.out.println(file2);
File parent = new File("D:\\java");
File file3 = new File(parent,"file3.txt");//File类的父路径、子路径
System.out.println(file3);

现在对 File 类进行总结

微信图片_20220417151411.jpg

基础 IO 类和相关方法

虽然. IO 类有很多,但是最基本的是四个抽象类,InputStream、OutputStream、Reader、Writer。最基本的方法也就是 read()write() 方法,其他流都是上面这四类流的子类,方法也是通过这两类方法衍生而成的。而且大部分的 IO 源码都是 native 标志的,也就是说源码都是 C/C++ 写的。这里我们先来认识一下这些流类及其方法

InputStream

InputStream 是一个定义了 Java 流式字节输入模式的抽象类。该类的所有方法在出错条件下引发一个IOException 异常。它的主要方法定义如下

微信图片_20220417151415.jpg

OutputStream

OutputStream 是定义了流式字节输出模式的抽象类。该类的所有方法返回一个void 值并且在出错情况下引发一个IOException异常。它的主要方法定义如下

微信图片_20220417151417.jpg

Reader 类

Reader 是 Java 定义的流式字符输入模式的抽象类。类中的方法在出错时引发 IOException 异常。

微信图片_20220417151421.jpg

Writer 类

Writer 是定义流式字符输出的抽象类。所有该类的方法都返回一个 void 值并在出错条件下引发 IOException 异常

微信图片_20220417151423.jpg

InputStream 及其子类

FileInputStream 文件输入流:FileInputStream 类创建一个能从文件读取字节的 InputStream 类

ByteArrayInputStream 字节数组输入流 :把内存中的一个缓冲区作为 InputStream 使用

PipedInputStream 管道输入流:实现了pipe 管道的概念,主要在线程中使用

SequenceInputStream 顺序输入流:把多个 InputStream 合并为一个 InputStream

FilterOutputStream 过滤输入流:其他输入流的包装。

ObjectInputStream 反序列化输入流 :将之前使用 ObjectOutputStream 序列化的原始数据恢复为对象,以流的方式读取对象

DataInputStream : 数据输入流允许应用程序以与机器无关方式从底层输入流中读取基本 Java 数据类型。

PushbackInputStream 推回输入流:缓冲的一个新颖的用法是实现推回 (pushback) 。Pushback 用于输入流允许字节被读取然后返回到流。

OutputStream 及其子类

FileOutputStream 文件输出流:该类实现了一个输出流,其数据写入文件。

ByteArrayOutputStream 字节数组输出流 :该类实现了一个输出流,其数据被写入由 byte 数组充当的缓冲区,缓冲区会随着数据的不断写入而自动增长。

PipedOutputStream 管道输出流 :管道的输出流,是管道的发送端。

ObjectOutputStream 基本类型输出流  :该类将实现了序列化的对象序列化后写入指定地方。

FilterOutputStream 过滤输出流:其他输出流的包装。

PrintStream 打印流 通过 PrintStream 可以将文字打印到文件或者网络中去。

DataOutputStream : 数据输出流允许应用程序以与机器无关方式向底层输出流中写入基本 Java 数据类型。

Reader 及其子类

FileReader 文件字符输入流 :把文件转换为字符流读入

CharArrayReader 字符数组输入流 :是一个把字符数组作为源的输入流的实现

BufferedReader 缓冲区输入流 :BufferedReader 类从字符输入流中读取文本并缓冲字符,以便有效地读取字符,数组和行

PushbackReader: PushbackReader 类允许一个或多个字符被送回输入流。

PipedReader 管道输入流:主要用途也是在线程间通讯,不过这个可以用来传输字符

Writer 及其子类

FileWriter 字符输出流 :FileWriter 创建一个可以写文件的 Writer 类。

CharArrayWriter 字符数组输出流:CharArrayWriter 实现了以数组作为目标的输出流。

BufferedWriter 缓冲区输出流 :BufferedWriter是一个增加了flush( ) 方法的Writer。flush( )方法可以用来确保数据缓冲器确实被写到实际的输出流。

PrintWriter :PrintWriter 本质上是 PrintStream 的字符形式的版本。

PipedWriter 管道输出流:主要用途也是在线程间通讯,不过这个可以用来传输字符

Java 的输入输出的流式接口为复杂而繁重的任务提供了一个简洁的抽象。过滤流类的组合允许你动态建立客户端流式接口来配合数据传输要求。继承高级流类 InputStream、InputStreamReader、 Reader 和 Writer 类的 Java 程序在将来 (即使创建了新的和改进的具体类)也能得到合理运用。

深入理解 Java IO ,你可以阅读作者的这篇文章 深入理解 Java IO

注解

Java 注解(Annotation) 又称为元数据 ,它为我们在代码中添加信息提供了一种形式化的方法。它是 JDK1.5 引入的,Java 定义了一套注解,共有 7 个,3 个在 java.lang 中,剩下 4 个在 java.lang.annotation 中。

作用在代码中的注解有三个,它们分别是

  • @Override:重写标记,一般用在子类继承父类后,标注在重写过后的子类方法上。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
  • @Deprecated :用此注解注释的代码已经过时,不再推荐使用
  • @SuppressWarnings:这个注解起到忽略编译器的警告作用

元注解有四个,元注解就是用来标志注解的注解。它们分别是

  • @Retention: 标识如何存储,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。

RetentionPolicy.SOURCE:注解只保留在源文件,当 Java 文件编译成class文件的时候,注解被遗弃;

RetentionPolicy.CLASS:注解被保留到 class 文件,但 jvm 加载 class 文件时候被遗弃,这是默认的生命周期;

RetentionPolicy.RUNTIME:注解不仅被保存到 class 文件中,jvm 加载 class 文件之后,仍然存在;

  • @Documented: 标记这些注解是否包含在 JavaDoc 中。
  • @Target:标记这个注解说明了 Annotation 所修饰的对象范围,Annotation 可被用于 packages、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)。取值如下
public enum ElementType {
    TYPE,       // 类、接口、注解、枚举
    FIELD,      // 字段
    METHOD,      // 方法
    PARAMETER,    // 参数
    CONSTRUCTOR,   // 构造方法
    LOCAL_VARIABLE,  // 本地变量
    ANNOTATION_TYPE, // 注解
    PACKAGE,     // 包
    TYPE_PARAMETER,  // 类型参数
    TYPE_USE     // 类型使用
  • @Inherited :标记这个注解是继承于哪个注解类的。

从 JDK1.7 开始,又添加了三个额外的注解,它们分别是

  • @SafeVarargs :在声明可变参数的构造函数或方法时,Java 编译器会报 unchecked 警告。使用 @SafeVarargs 可以忽略这些警告
  • @FunctionalInterface: 表明这个方法是一个函数式接口
  • @Repeatable:标识某注解可以在同一个声明上使用多次。

注意:注解是不支持继承的。

注解的生命周期

注解也是有相应的声明周期的,也是封装在一个枚举类:RetentionPolicy 中:

  • SOURCE:源代码期间,在编译时会去除,所以这都是给编译器使用的。
  • CLASS:会保留在类文件中,但是运行时 JVM 不需要保存,默认的生命周期。
  • RUNTIME:会持续保存到 JVM 运行时,可以通过反射来获取。

声明周期配合 @Retention 来使用,使用方法如下:

@Retention(RetentionPolicy.RUNTIME)

一般来说对于编写框架用的注解的生命周期都是RUNTIME。

关于 null 的几种处理方式

对于 Java 程序员来说,空指针一直是恼人的问题,我们在开发中经常会受到 NullPointerException 的蹂躏和壁咚。Java 的发明者也承认这是一个巨大的设计错误。

那么关于 null ,你应该知道下面这几件事情来有效的了解 null ,从而避免很多由 null 引起的错误。

微信图片_20220417151430.jpg

大小写敏感

首先,null 是 Java 中的关键字,像是 **public、static、final。**它是大小写敏感的,你不能将 null 写成 Null 或 NULL,编辑器将不能识别它们然后报错。

微信图片_20220417151434.jpg

这个问题已经几乎不会出现,因为 eclipse 和 Idea 编译器已经给出了编译器提示,所以你不用考虑这个问题。

null 是任何引用类型的初始值

null 是所有引用类型的默认值,Java 中的任何引用变量都将null作为默认值,也就是说所有 Object 类下的引用类型默认值都是 null。这对所有的引用变量都适用。就像是基本类型的默认值一样,例如 int 的默认值是 0,boolean 的默认值是 false。

下面是基本数据类型的初始值

微信图片_20220417151437.jpg

null 只是一种特殊的值

null 既不是对象也不是一种类型,它仅是一种特殊的值,你可以将它赋予任何类型,你可以将 null 转换为任何类型

public static void main(String[] args) {
  String str = null;
  Integer itr = null;
  Double dou = null;
  Integer integer = (Integer) null;
  String string = (String)null;
  System.out.println("integer = " + integer);
  System.out.println("string = " + string);
}

你可以看到在编译期和运行期内,将 null 转换成任何的引用类型都是可行的,并且不会抛出空指针异常。

null 只能赋值给引用变量,不能赋值给基本类型变量

持有 null 的包装类在进行自动拆箱的时候,不能完成转换,会抛出空指针异常,并且 null 也不能和基本数据类型进行对比

public static void main(String[] args) {
  int i = 0;
  Integer itr = null;
  System.out.println(itr == i);
}

使用了带有 null 值的引用类型变量,instanceof 操作会返回 false

public static void main(String[] args) {
  Integer isNull = null;
  // instanceof = isInstance 方法
  if(isNull instanceof Integer){
    System.out.println("isNull is instanceof Integer");
  }else{
    System.out.println("isNull is not instanceof Integer");
  }
}

这是 instanceof 操作符一个很重要的特性,使得对类型强制转换检查很有用

静态变量为 null 调用静态方法不会抛出 NullPointerException。因为静态方法使用了静态绑定

使用 Null-Safe 方法

你应该使用 null-safe 安全的方法,java 类库中有很多工具类都提供了静态方法,例如基本数据类型的包装类,Integer , Double 等。例如

public class NullSafeMethod {
    private static String number;
    public static void main(String[] args) {
        String s = String.valueOf(number);
        String string = number.toString();
        System.out.println("s = " + s);
        System.out.println("string = " + string);
    }
}

number 没有赋值,所以默认为null,使用String.value(number) 静态方法没有抛出空指针异常,但是使用 toString()却抛出了空指针异常。所以尽量使用对象的静态方法。

null 判断

你可以使用 == 或者 != 操作来比较 null 值,但是不能使用其他算法或者逻辑操作,例如小于或者大于。跟SQL不一样,在Java中 null == null 将返回 true,如下所示:

public class CompareNull {
    private static String str1;
    private static String str2;
    public static void main(String[] args) {
        System.out.println("str1 == str2 ? " + str1 == str2);
        System.out.println(null == null);
    }
}

创建对象的几种方式

使用 new 来创建对象

使用 new 来创建对象是最简单的一种方式了,new 是 Java 中的关键字,new 通过为新对象分配内存并返回对该内存的引用来实例化一个类,这个实例化一个类其实就相当于创建了一个对象,因为类也是一种对象;new 也负责调用对象的构造函数,下面是使用 new 来创建对象的代码

Object obj = new Object();

这段代码中,我们在堆区域中分配了一块内存,然后把 obj 对象指向了这块内存区域。

不知道你有没有看过 new 的字节码呢?下面是这段代码的字节码

微信图片_20220417151443.jpg

在 Java 中,我们认为创建一个对象就是调用其构造方法,所以我们使用 new Object() 构造的对象,其实是调用了 Object 类的无参数 的构造方法。但是通过字节码我们发现,对象的创建和调用其构造方法是分开的。

字节码的 new 表示在堆中创建一个对象,并把对象的引用推入栈中。invokespecial 表示调用对象无参数的构造方法。其实,JVM 提供了五种方法调用指令,分别是

  • invokestatic:该指令用于调用静态方法,即使用 static 关键字修饰的方法;
  • invokespecial:该指令用于三种场景:调用实例构造方法,调用私有方法(即 private 关键字修饰的方法)和父类方法(即 super 关键字调用的方法);
  • invokeinterface:该指令用于调用接口方法,在运行时再确定一个实现此接口的对象;
  • invokevirtual:该指令用于调用虚方法(就是除了上述三种情况之外的方法);
  • invokedynamic:在运行时动态解析出调用点限定符所引用的方法之后,调用该方法;在 JDK 1.7 中提出,主要用于支持 JVM 上的动态脚本语言(如 Groovy,Jython 等)

好了,现在你知道了 new 和 invokespecial 是干啥用的,那么 dup 指令呢?

dup 会复制栈上的最后一个元素,然后再次将其推入栈;因此,如果在栈上有一个对象引用,并且调用了 dup,则现在在栈上有对该对象的两个引用。看起来有点不知其所以然,所以在求助网上的时候,又发现了 R 大的解释

微信图片_20220417151448.jpg

来源:https://www.zhihu.com/question/52749416

后面的 astore 就会把操作数栈顶的那个引用消耗掉,保存到指定的局部变量去。

如果直接使用 new Object() 没有创建局部变量的话,请注意一下它的字节码。

微信图片_20220417151451.jpg

看出来细微的差别了吗?上图中的 astore_1 竟然变成了 pop,这也就是说,new Object() 没有保存对象的局部变量,而是直接把它给消耗掉了。嗯,符合预期。

所以这是第一种创建的方式,也就是使用 new 来创建。

使用 newInstance 方法来创建

这个newInstance 方法指的是 class 类中的方法,newInstance 方法会调用无参的构造方法创建对象。

我们可以使用 newInstance 方法创建对象,下面是使用示例代码

User user = (User)Class.forName("com.cxuan.test.User").newInstance();
// 或者使用
User user = User.class.newInstance();

下面我们分析一下这个字节码,其实使用第一种方式和第二种方式就差了一个 Class.forName 的字节码,这是一个静态方法,应该用的是 invokestatic,下面我们验证一下。

第一种方式的字节码

微信图片_20220417151455.jpg

第二种方式的字节码

微信图片_20220417151457.jpg

可以看到,我们验证的是正确的。

那么这段字节码是什么意思呢?

ldc 的意思是把常量池中的引用推入到当前堆栈中,invokestatic 和 invokevirtual 我们上面解释过了,然后就是 checkcast, 这个字节码的含义就是进行类型转换,因为 newInstance 生成的是一个 Object 的对象,所以我们需要把它转换为我们需要的 User 类型,这个字节码就是干这个活的。

使用反射来创建对象

使用反射来创建对象其实也是使用了 newInstance 方法,只不过这个方法是 Constructor ,Java 反射中构造器的方法,我们可以通过这种方式来创建一个新的对象。如下代码所示

Constructor<User> constructor = User.class.getConstructor();
User user = constructor.newInstance();

下面是它的字节码

微信图片_20220417151501.jpg

这里解释下 iconst_0 ,它的意思就是将 int 值 0 加载到堆栈上,这个相当于是为 getConstructor 方法准备参数分配的字节码。

为了验证这个结论,我们从简优化,看一下其他方法的字节码

User.class.getDeclaredField("id");

它的字节码如下:

微信图片_20220417151504.jpg

可以看到,第二个 ldc 其实就是 getDeclaredField 中的参数,为 String 类型,所以是用的 ldc,它是将引用推入堆栈。

使用对象克隆来创建对象

这是第四种创建方式,使用 Cloneable 类中的 clone() 方法来创建,它的前提是你需要实现 Cloneable 接口并实现其定义的 clone 方法。用 clone 方法创建对象并不会调用任何构造函数。

如下代码所示

Constructor<User> constructor = User.class.getConstructor();
User user = constructor.newInstance();
user.setName("cxuan");
User user2 = (User)user.clone();
System.out.println(user2.getName());

输出 cxuan

它的字节码如下

微信图片_20220417151507.jpg

这个字节码有些长,但是字节码的概念和含义我们上面已经介绍过了,最主要的就是推入堆栈,调用对应的实例方法。

对象克隆这块是面试官非常喜欢考的一个点,我后面会解析一下浅拷贝和深拷贝的区别。

使用反序列化创建对象

当我们使用序列化和反序列化时,JVM 也会帮我们创建一个单独的对象。在反序列化时,JVM 创建对象不会调用任何构造函数,如下代码所示

ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("xxx"));
out.writeObject(user2);
out.close();
//Deserialization
ObjectInputStream in = new ObjectInputStream(new FileInputStream("xxx"));
User user3 = (User) in.readObject();
in.close();
user3.setName("cxuan003");
System.out.println(user3 + ", hashcode : " + user3.hashCode());

这段反编译过后的字节码文件比较长,我这里就先不贴出来了,读者们可以自己编译看一下,其实并没有特别的字节码指令,大部分我们上面已经提到过了。

相关文章
|
5月前
|
存储 缓存 Java
最新Java基础系列课程--Day10-IO流文件处理
最新Java基础系列课程--Day10-IO流文件处理
|
5月前
|
存储 Java
最新Java基础系列课程--Day10-IO流文件处理(一)
最新Java基础系列课程--Day10-IO流文件处理
|
4月前
|
Java
【Java基础】输入输出流(IO流)
Java基础、输入输出流、IO流、流的概念、输入输出流的类层次结构图、使用 InputStream 和 OutputStream流类、使用 Reader 和 Writer 流类
144 2
|
21天前
|
安全 Java API
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
String常量池、String、StringBuffer、Stringbuilder有什么区别、List与Set的区别、ArrayList和LinkedList的区别、HashMap底层原理、ConcurrentHashMap、HashMap和Hashtable的区别、泛型擦除、ABA问题、IO多路复用、BIO、NIO、O、异常处理机制、反射
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
|
4月前
|
安全 Java
|
4月前
|
搜索推荐 算法 Java
【Java基础】 几种简单的算法排序
几种简单的JAVA算法排序
45 4
|
4月前
|
存储 缓存 Java
Java基础17-读懂Java IO流和常见面试题(二)
Java基础17-读懂Java IO流和常见面试题(二)
39 0
|
4月前
|
存储 Java Unix
Java基础17-读懂Java IO流和常见面试题(一)
Java基础16-读懂Java IO流和常见面试题(一)
58 0
|
5月前
|
Java
Java基础教程(12)-Java中的IO流
【4月更文挑战第12天】Java IO涉及输入输出,包括从外部读取数据到内存(如文件、网络)和从内存输出到外部。流是信息传输的抽象,分为字节流和字符流。字节流处理二进制数据,如InputStream和OutputStream,而字符流处理Unicode字符,如Reader和Writer。File对象用于文件和目录操作,Path对象简化了路径处理。ZipInputStream和ZipOutputStream则用于读写zip文件。
|
5月前
|
搜索推荐 Java
Java基础(快速排序算法)
Java基础(快速排序算法)
41 4
下一篇
无影云桌面