JavaSE进阶(常见类,集合源码,Stream,文件IO)

Java常见类

Obeject

Object是所有类的父类

方法说明
Object clone()创建与该对象的类相同的新对象
boolean equals(Object)比较两个对象是否相等。默认比较的是地址值。
void finalize()当垃圾回收器确定不存在对该对象的更多引用时,对象的垃圾回收器调用该方法
Class getClass()返回一个对象运行时的实例类(.class文件)
int hashCode()返回该对象的散列码值
void notify()激活等待在该对象的监视器上的一个线程
void notifyAll()激活等待在该对象的监视器上的全部线程
String toString()返回该对象的字符串表示,默认返回运行时类名+@+对象的hashCode的16进制数
void wait()在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待

克隆对象需要实现Cloneable接口,否则会报错:java.lang.CloneNotSupportedException

3个常用方法

toString()

默认输出类名和它的hash值

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

equal(Object o)

默认比较的是两个对象的地址值

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

hashCode()

public native int hashCode();

String & StringBuilder & StringBuffer

String

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
  
  private final char value[];
  ....
}

从源码我们可以看出String和它的属性value[]都是final类型的,即不可变类型,地址不变,字符串也不变

常用方法不介绍,大家可以看一波源码哈哈哈

public class StringTest02 {
      public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "world";
        String s3 = "helloworld";

        System.out.println(s3 == s1+s2);                  //false
        System.out.println(s3.equals(s1+s2));             //true

        System.out.println(s3 == "hello"+"world");        //true
        System.out.println(s3.equals("hello"+"world"));   //true
      }
}

我们来重点解析一下这一段,首先我们来看一下两个equal为什么都是true,直接上源码

public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
}

我们发现,这是每个字符逐一比较的,所以只要两个字符串一样就是true

我们再来看一下两个==为什么结果不一样

因为字符串做连接操作

  • 如果是两个变量进行连接,先开辟空间,再连接

img

  • 如果是两个常量进行连接,先连接,获取连接结构值。然后在常量池里面查找有没有这个结果值,如果有,直接给地址值;没有,开辟空间

这样我们就可以解释上面两个==不同的原因了,第一个==是两个变量的连接,所以先开辟空间,再放入两个连接以后的字符串,所以第一个==比较的是地址。再来看看第二个==,常量连接,连接后的值检索常量池,发现存在helloworld,直接把helloworld的地址给它,两个相同地址一比较当然就是true了

尝试从JVM角度理解

String str=new String(“xyz”);一共创建了几个String对象?

2个,一个在常量池,一个堆中,但两个的hashCode都是一样的,具体原理尚不可知

==常量池和堆中对象的区别(留坑)==

image-20201110115357439

public class Hello {
    public static void main(String[] args) {
        String str="hello world";
        System.out.println(str.hashCode());
        System.out.println("hello world".hashCode());
        
    }
}
/*
1794106052
1794106052
*/

StringBuilder & StringBuffer

img

abstract class AbstractStringBuilder implements Appendable, CharSequence {
        /**
         * The value is used for character storage.
         */
        char[] value;
  			....
}
public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
}
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin);

从一段源码中我们可以看出几件事,相比于String,StringBuilder和StringBuffer的父类AbstractStringBuilder的value[]并不是final类型,所以它是可变的,这也是String和他们最大的区别,通过append(String str)这个方法我们可以证实这一点,append中扩大了value[]的容量,然后将str中拷贝到value中conunt位置,这不就完成了添加的操作了嘛!

三者的区别

StringStringBufferStringBuilder
String的值是不可变的,这就导致每次对String的操作都会生成新的String对象,不仅效率低下,而且浪费大量优先的内存空间StringBuffer是可变类,和线程安全的字符串操作类,任何对它指向的字符串的操作都不会产生新的对象。每个StringBuffer对象都有一定的缓冲区容量,当字符串大小没有超过容量时,不会分配新的容量,当字符串大小超过容量时,会自动增加容量可变类,速度更快
不可变可变可变
线程安全线程不安全
多线程操作字符串单线程操作字符串

总结

少量增删改操作可以使用String,多的话使用StringBuilder,如果多线程情况下使用StringBuffer

参考文献

String、StringBuffer与StringBuilder之间区别

Scanner

构造方法&其他方法

public void test1() {
        Scanner scanner = new Scanner(System.in);//构造函数
        while (scanner.hasNextLine()) {//判断是否有下一行
            System.out.println(scanner.nextLine());//读取下一行
        }
}

Math&Random

方法和C/C++里常用的差不多其实,需要注意的点是,它的Mtah.random()是Random类的,并不是自己构造的,看一下源码就知道了

//这两个都是在Math类中的
public static double random() {
        return RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble();
}
private static final class RandomNumberGeneratorHolder {
        static final Random randomNumberGenerator = new Random();
}

Random():以当前时间毫秒值作为种子,创建Random对象

注意一下nextDouble和nextFloat这两个方法生成的都是0~1之间的数

public double nextDouble() {
        return (((long)(next(26)) << 27) + next(27)) * DOUBLE_UNIT;
}

private static final double DOUBLE_UNIT = 0x1.0p-53; // 1.0 / (1L << 53)

public float nextFloat() {
        return next(24) / ((float)(1 << 24));
}

时间类

Date

public void test2() {
        Date date = new Date();
        System.out.println(date);

        Date date1 = new Date(2020 - 1900, 10 - 1, 31);
        System.out.println(date1);

        System.out.println(date.getYear() + 1900 + " " + (date.getMonth() + 1));//获取年和月
}
//-1900 -1的原因
public Date(int year, int month, int date, int hrs, int min, int sec) {
        int y = year + 1900;//原因1
        // month is 0-based. So we have to normalize month to support Long.MAX_VALUE.
        if (month >= 12) {
            y += month / 12;
            month %= 12;
        } else if (month < 0) {
            y += CalendarUtils.floorDivide(month, 12);
            month = CalendarUtils.mod(month, 12);//原因2
        }
        BaseCalendar cal = getCalendarSystem(y);
        cdate = (BaseCalendar.Date) cal.newCalendarDate(TimeZone.getDefaultRef());
        cdate.setNormalizedDate(y, month + 1, date).setTimeOfDay(hrs, min, sec, 0);
        getTimeImpl();
        cdate = null;
}
/*
Sat Oct 31 16:11:35 CST 2020
Sat Oct 31 00:00:00 CST 2020
2020 10
*/

Calendar

貌似使用的是单例模式,所以构造函数隐藏了,只暴露了getInstance()方法

public void test3() {
        Calendar calendar = Calendar.getInstance();//获取实例对象
        System.out.println("初始:" + calendar.getTime());//获取本地时间
        calendar.set(2020, 10 - 1, 31, 4, 22, 34);//自定义时间
        System.out.println("set后:" + calendar.getTime());
        calendar.set(Calendar.YEAR, 2018);//设置自定义年
        System.out.println("set year后:" + calendar.getTime());
        calendar.add(Calendar.YEAR, 1);//+1年
        System.out.println("add后:"+calendar.getTime());
}
/*
初始:Sat Oct 31 16:28:29 CST 2020
set后:Sat Oct 31 04:22:34 CST 2020
set year:Wed Oct 31 04:22:34 CST 2018
add后:Thu Oct 31 04:22:34 CST 2019
*/

GregorianCalendar 是 Calendar 的一个具体子类,提供了世界上大多数国家使用的标准日历系统

GregorianCalendar有一个方法:boolean isLeapYear(int year) 确定给定的年份是否为闰年

SimpleDateFormat

格式化——String format(Date date)
解析——Date parse(String time)

img

看懂这个图就好了,下面附赠一个实例,Calendar和String互转的

public void test4() throws ParseException {
        //Calendar转String
        Calendar calendar = Calendar.getInstance();
        Date date = new Date(100000L);
        date.setTime(calendar.getTimeInMillis());
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        System.out.println(sdf.format(date));

        //String转Calendar
        String str = "2019-10-31";
        SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd");
        Date date1 = sdf1.parse(str);
        Calendar calendar1 = Calendar.getInstance();
        calendar1.setTime(date1);
        System.out.println(calendar1.getTime());
}
/*
2020-10-31
Thu Oct 31 00:00:00 CST 2019
*/

System

这个仅做了解好吧。

构造方法

没有构造方法

成员方法

gc():运行垃圾回收处理机制(系统会在某个不确定的时间调用该方法)会调用finalize(),进行垃圾回收

exit(int status):退出JVM,0表示非异常退出

currentTimeMills():获取当前时间毫秒值

arrayCopy(Object[] srcArr,int srcPos,Object[] desArr,int destPos,int len):数组复制

BigInteger&BigDecimal

刷题碰到大数题这两个类效果贼好.....

构造方法

BigInteger(String s):通过字符串创建BigInteger对象

BigDecimal(String s):通过字符创建BigDecimal对象

成员方法

  • Integer:

    add(BigInteger bi):+

    subtract(BigInteger bi):-

    multiply(BigInteger bi):*

    divide(BigInteger bi):/

  • Decimal:

    add(BigDecimal bi):+

    subtract(BigDecimal bi):-

    multiply(BigDecimal bi):*

    divide(BigDecimal bi):/

包装类

基本定义

包装类即使把基本类型变成对象类型,包含每种基本数据类型的相关属性如最大值、最小值等,以及相关的操作方法

数据类型浮点型大小(占字节数,2的几次方)范围默认值包装器类型
byte(字节)8-128 - 1270Byte
shot(短整型)16-32768 - 327680Short
int(整型)32-2147483648-21474836480Integer
long(长整型)64-9233372036854477808-92333720368544778080Long
float(浮点型)32-3.40292347E+38-3.40292347E+380.0fFloat
double(双精度)64-1.79769313486231570E+308-1.79769313486231570E+3080.0dDouble
char(字符型)16‘ \u0000 - u\ffff ’‘\u0000 ’Character
boolean(布尔型)1true/falsefalseBoolean

包装类转换关系

  • 基本类型 --> 包装器类

Integer obj=new Integer(10);

  • 包装器类 --> 基本类型

    int num=obj.intValue();

  • 字符串 --> 包装器类

    Integer obj=new Integer("100");

  • 字符串 --> 基本类型

    int num=Integer.parseInt("-45.36");

public void test5() {
        Integer integer = new Integer(100);
        System.out.println(integer);
        Integer integer1 = new Integer("100000");
        System.out.println(integer1);
        int integer2 = integer1.intValue();
        System.out.println(integer2);
        int integer3 = Integer.parseInt("100000");
        System.out.println(integer3);
}

拆包&装包(留坑)

集合(Collection&Map)

学习路线

1.集合关系图

image-20201030170001038

image-20201030170055110

2.杂谈

IDEA中diagram:实线——继承;虚线——实现(部分/全部)接口

image-20201030170404585

ArrayList分析

数组,线程不安全,效率高

1.源码分析

private static final int DEFAULT_CAPACITY = 10;//默认数组大小
transient Object[] elementData; // 数据存放的数组

// 数组的动态增长
private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
  			//扩大1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
  			// 最大数组长度规定,newCapacity - MAX_ARRAY_SIZE > 0时,通过minCapacity试探最大长度
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
  			// Arrays.copyOf实现原数组到新数组的拷贝以及扩容
        elementData = Arrays.copyOf(elementData, newCapacity);
}

// 如果newCapacity超过MAX_ARRAY_SIZE,但minCapacity又没有超过MAX_ARRAY_SIZE,就规定newCapacity=MAX_ARRAY_SIZE;
// 如果两个都大于MAX_ARRAY_SIZE,就规定newCapacity=Integer.MAX_VALUE;
private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
          throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
          Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

其余方法都是正常思路,所以就不分析了(可能也是我没看到.....)

2.注意点

有序,有下标,元素可重复
remove有两种形式的重载
boolean remove(Object o);
Object remove(int index);
如果是ArrayList<Integer>,传入数字,会默认是Object remove(int index);如果希望使用第一种应该:remove(new Integer(123));

LinkedList分析

双向链表,同时也是链式双端队列,可以通过上面的继承图看出来

源码分析

transient int size = 0;		//链表大小
transient Node<E> first;	// 头节点
transient Node<E> last;		// 尾节点

看一下结构,其他的不赘述,可以自己去看源码哈哈哈....

Vector&Stack分析

数组,线程安全,效率低

//Stack的search返回的位置是从栈顶开始的偏移量,不过从下标从1开始
public synchronized int search(Object o) {
        int i = lastIndexOf(o);

        if (i >= 0) {
            return size() - i;
        }
        return -1;
}

//测试代码
public void test3() {
        Stack<Integer> stack = new Stack<>();
        stack.push(10);
        stack.push(20);
        stack.push(30);
        stack.push(40);
        stack.push(50);
        System.out.println(stack);
        System.out.println(stack.search(10));
        System.out.println(stack.search(50));
}
//结果
/*
[10, 20, 30, 40, 50]
5
1
*/

HashSet分析

(基于HashMap实现)——数组,链表,红黑树

1.存储过程

1)根据hashcode计算保存的位置,如果位置唯空直接保存,不为空进行2)

2)与hashcode位置上的链表逐一比较,equal相同则重复不插入,不相同则插入

2.继承图

image-20201030210215629

TreeSort分析

1.自定义排序

Comparator接口中的int compare(Object o1 , Object o2)实现自定义排序

  • 如果返回 1 说明o1 > o2 如 2 1
  • 如果返回 0 说明o1 = o2 如 cc cc
  • 如果返回 -1 说明o1 < o2 如 6 7

比HashSet多了一层Sort排序规则

2.继承图

image-20201030210849864

PriorityQueue分析

堆实现

1.源码

//插入,插入到堆的尾端,再自底向上调整堆
private void siftUpUsingComparator(int k, E x) {
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            Object e = queue[parent];
            if (comparator.compare(x, (E) e) >= 0)
                break;
            queue[k] = e;
            k = parent;
        }
        queue[k] = x;
}
//删除,删除堆顶元素,使用堆尾元素替换堆顶,再自顶向下调整堆
private void siftDownComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>)x;
        int half = size >>> 1;        // loop while a non-leaf
  			// k<half作为判断条件是因为没必要从叶子结点开始,从最后一个叶子节点的父亲节点开始调整最好
        while (k < half) {
            int child = (k << 1) + 1; // assume left child is least
            Object c = queue[child];
            int right = child + 1;
            if (right < size &&
                ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
                c = queue[child = right];
            if (key.compareTo((E) c) <= 0)
                break;
            queue[k] = c;
            k = child;
        }
        queue[k] = key;//找到确定位置替换
}

2.注意点

public void test() {
        Queue<String> queue = new PriorityQueue<>();
        queue.add("c");
        queue.add("d");
        queue.add("a");
        queue.add("b");
        queue.add("e");
        queue.add("f");
        System.out.println(queue);
        queue.remove("b");
        //queue.poll();
        System.out.println(queue);
}
/* 结果
[a, b, c, d, e, f]
[a, d, c, f, e]
*/

remove操作会改变堆的有序性,所以少用???

可能因为优先队列本身就不支持对不是堆头的元素进行,同时也不保证修改好仍然有效???

ArrayDeque源码分析

实现Deque(Interface)接口,所以属于顺序双端队列

1.循环队列复习

front指向当前队头,rear指向队尾,即元素插入的位置(空位置,并非最后队列最后一个元素)

为了避免front==rear的二义性,所以需要人为的空出一个位置来区分空和满的判断条件

  • 判满
    (rear+1)%MAXSIZE==front;
  • 插入
    data[rear]=X;
    rear=(rear+1)%MAXSIZE;
  • 判空
    front==rear;
  • 删除
    front=(font+1)%MAXSIZE;

2.源码

循环队列复习其实和源码分析并没有什么关系,因为由于ArrayDeque是动态的,所以不会有空和满的二义性,所以不需要人为空一个位置判断队列满

插入

/*
往队头添加元素,head=0,但head-1以后就为-1
与length-1取&后为length-1
因为-1的二进制为1111...111,而length为2的幂指数,2^n - 1以后肯定也是1111...111,所以这里的head从length-1开始递减(自己按插入顺序理解一下即可)
*/
public void addFirst(E e) {
        if (e == null)
            throw new NullPointerException();
        elements[head = (head - 1) & (elements.length - 1)] = e;
        if (head == tail)
            doubleCapacity();
}
/*
往队尾添加元素一样的道理,tail从0开始递增,直到碰到head就扩大length两倍,不同于动态数组1.5倍
*/
public void addLast(E e) {
        if (e == null)
            throw new NullPointerException();
        elements[tail] = e;
        if ( (tail = (tail + 1) & (elements.length - 1)) == head)
            doubleCapacity();
}

删除

/*
插入head-1,删除head+1,如果超过length-1就会变为0,删除tail插入的元素,直到再次弹出的元素是null就代表队列空了
*/
public E pollFirst() {
        int h = head;
        @SuppressWarnings("unchecked")
        E result = (E) elements[h];
        // Element is null if deque empty
        if (result == null)
            return null;
        elements[h] = null;     // Must null out slot
        head = (h + 1) & (elements.length - 1);
        return result;
}
/*
因为tail指向的是即将要插入的位置,那里并没有元素,所以要先做-1操作,其余和pollFirst一致,不赘述
*/
public E pollLast() {
        int t = (tail - 1) & (elements.length - 1);
        @SuppressWarnings("unchecked")
        E result = (E) elements[t];
        if (result == null)
            return null;
        elements[t] = null;
        tail = t;
        return result;
}

HashMap源码剖析(JDK1.8)

存储结构

数组+链表+红黑树

在这里插入图片描述

 //链表节点,继承了Map的键值对
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;//下一个节点

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
}

构造函数

/**
 * initialCapacity	初始容量(假)
 * threshold				所能容纳的key-value对极限(真容量,2^n,超过就扩容)
 * loadFactor				负载因子(一般为0.75)
 * size							存在的键值对个数
 * length						table的长度大小
 */
public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
}

注意3个变量的区别:

二进制运算

1. HashMap的容量大小计算

 /**
  * Returns a power of two size for the given target capacity.
  * 获取大于给定容量的最小2次幂指数,tableSizeFor(10)=16;
  */
static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

最精华的位运算,模拟一遍

假设cap=(0001 ****)

0001 **** >>1

0000 1***


0001 1*** >>2

0000 011*


0001 111* >>4

0000 0001


0001 1111 >>8

0000 0000


0001 1111 >>16

.....

通过以上的模拟,可以发现,刚开始>>1,将两位确定成1,再>>2,将4位确定成1,再>>4,将8确定成1,......以此类推

最后通过+1将n确定成大于给定容量的最小2次幂指数

2. 确定哈希桶数组索引位置

// h = key.hashCode() 为第一步 取hashCode值
// h ^ (h >>> 16)  为第二步 高位参与运算
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//这一步没有另外声明一个函数,而是融合在了代码中
static int indexFor(int h, int length) {  //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的
     return h & (length-1);  //第三步 取模运算
}

在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = key.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。

h=key.hashCode() 1111 1111 1111 1111 1111 0000 1110 0001

h>>16 0000 0000 0000 0000 1111 1111 1111 1111


hash=h^(h>>16) 1111 1111 1111 1111 0000 1111 0001 1110

length-1 0000 0000 0000 0000 0000 0000 1111 1111


hash&(length-1) 0000 0000 0000 0000 0000 0000 0001 1110

​ 30

3.确定扩容后的位置

newTab[e.hash & (newCap - 1)] = e;

newCap=2*oldCap

hash 0101 1101

oldCap 0001 0000

oldCap-1 0000 1111

hash&(oldCap-1) 0000 1101 =>14


newCap 0010 0000

newCap-1 0001 1111

hash&(newCap-1) 0001 1101 =>30

可以发现新位置=旧位置 | (旧位置+oldCap)

HashMap的put方法

在这里插入图片描述
//key存在,返回对应的被覆盖的key的value;key不存在,返回null
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
  			//判断table是否为空||length=0,是就扩容
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
  			//table[i]==null直接插入
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
          	//判断key是否存在(只是在table[i]这个位置)
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
          	//判断是否属于treeNode对象
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//key存在返回存在的Node,不存在返回null
            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(tab, hash);
                        break;
                    }
                  	//一样的就替换
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
              	//替换重复节点的value并返回
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
  			//如果键值对个数大于极限,就扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
}

扩容机制

1.JDK1.7扩容源码

void resize(int newCapacity) {   //传入新的容量
      Entry[] oldTable = table;    //引用扩容前的Entry数组
      int oldCapacity = oldTable.length;         
      if (oldCapacity == MAXIMUM_CAPACITY) {  //扩容前的数组大小如果已经达到最大(2^30)了
          threshold = Integer.MAX_VALUE; //修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
          return;
      }

      Entry[] newTable = new Entry[newCapacity];  //初始化一个新的Entry数组
      transfer(newTable, initHashSeedAsNeeded(newCapacity));                         //!!将数据转移到新的Entry数组里
      table = newTable;                           //HashMap的table属性引用新的Entry数组
      threshold = (int)(newCapacity * loadFactor);//修改阈值
}

void transfer(Entry[] newTable, boolean rehash) {
      int newCapacity = newTable.length;
      // 遍历旧数组得到每一个key再根据新数组的长度重新计算下标存进去,如果是一个链表,则链表中的每个键值对也都要重新hash计算索引
      for (Entry<K,V> e : table) {
        // 如果此slot上存在元素,则进行遍历,直到e==null,退出循环
        while(null != e) {
          Entry<K,V> next = e.next;
          // 当前元素总是直接放在数组下标的slot上,而不是放在链表的最后,所以扩容后的链表和旧的链表顺序是相反的
          // 是否rehash
          if (rehash) {
            e.hash = null == e.key ? 0 : hash(e.key);
          }
          int i = indexFor(e.hash, newCapacity);
          // 把原来slot(槽)上的元素作为当前元素的下一个(即如果哈希冲突,将当前元素插入到之前元素的前面)
          e.next = newTable[i];
          // 新迁移过来的节点直接放置在slot(槽)位置上
          newTable[i] = e;
          e = next;
        }
      }
}

在这里插入图片描述

2.JDK1.8扩容机制

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) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            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;
                  	//oldTab[j]只有一个节点,就直接替换到newTab[j]中
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                  	//红黑树的resize
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                  	//链表优化resize
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                      	//区别于JDK1.7,这里链表的resize不使用头插法,而是采用尾插法
                        do {
                            next = e.next;
                          	//原索引,注意这里是&oldCap
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                          	//原索引+oldCap
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;//原索引
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;//原索引+oldCap
                        }
                    }
                }
            }
        }
        return newTab;//返回新表
}

线程安全(留坑)

结论:HashMap是线程不安全的,不要在并发的环境中同时操作HashMap,建议使用ConcurrentHashMap

参考文献

[java]HashMap 一遍就懂!!!!

LinkedHashMap的源码简要分析

功能

在HashMap的基础上可以记录元素插入的顺序

public void test() {
        Map<String, Integer> map = new HashMap<>();
        map.put("121233", 321);
        map.put("1asd22134", 321);
        map.put("12s51231", 321);
        System.out.println(map);

        Map<String, Integer> map1 = new LinkedHashMap<>();
        map1.put("121233", 321);
        map1.put("1asd22134", 321);
        map1.put("12s51231", 321);
        System.out.println(map1);
}
/* 结果
{12s51231=321, 1asd22134=321, 121233=321}
{121233=321, 1asd22134=321, 12s51231=321}
*/

存储结构

HashMap+双向链表——双向链表用于记录插入的顺序

static class Entry<K,V> extends HashMap.Node<K,V> {
      Entry<K,V> before, after;
      Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
      }
}

/**
 * The head (eldest) of the doubly linked list.
 */
transient LinkedHashMap.Entry<K,V> head;

/**
 * The tail (youngest) of the doubly linked list.
 */
transient LinkedHashMap.Entry<K,V> tail;

1.宏观结构

img

2.微观结构

img

链表建立过程

img

// LinkedHashMap覆盖了HashMap的Node<K,V> newNode(int hash, K key, V value, Node<K,V> e)
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
      LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<K,V>(hash, key, value, e);
      linkNodeLast(p);
      return p;
}
// 通过linkNodeLast维护链表结构
// link at the end of list
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
      LinkedHashMap.Entry<K,V> last = tail;
      tail = p;
      if (last == null)
        head = p;
      else {
        p.before = last;
        last.after = p;
      }
}

链表节点删除过程

//HashMap删除节点源码
final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)
                    tab[index] = node.next;
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);//重点看这里,HashMap中是个空方法
                return node;
            }
        }
        return null;
}
void afterNodeRemoval(Node<K,V> p) { }

//LinkedHashMap继承了HashMap,LinkedHashMap会执行remove操作时会调用Hashmap的remove,从而再回调LinkedHashMap中的void afterNodeRemoval(Node<K,V> p);
void afterNodeRemoval(Node<K,V> e) { // unlink
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.before = p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a == null)
            tail = b;
        else
            a.before = b;
}

就很普通的双向链表删除....跳过

链表的查找

1.结果

先看一段代码和它返回的结果,可以很明显发现,被访问的节点被链表移到了表尾

依靠的参数是accessOrder:访问顺序,也就是LinkedHashMap构造函数的第三个参数

public void test1() {
        Map<String, Integer> map = new HashMap<>();
        map.put("121233", 321);
        map.put("1asd22134", 321);
        map.put("12s51231", 321);
        System.out.println(map);

        Map<String, Integer> map1 = new LinkedHashMap<String, Integer>(16, 0.75f, true);

        String str = "121233";
        map1.put(str, 321);
        map1.put("1asd22134", 321);
        map1.put("12s51231", 321);
        System.out.println(map1);

        Integer val = map1.get(str);


        System.out.println(map1);
}
/*
{12s51231=321, 1asd22134=321, 121233=321}
{121233=321, 1asd22134=321, 12s51231=321}
{1asd22134=321, 12s51231=321, 121233=321}
*/

2.源码

public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
}
void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        if (accessOrder && (last = tail) != e) {
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
          	//头节点
            if (b == null)
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else
                last = b;
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            tail = p;
            ++modCount;
        }
}

可以看到源码还是调用HashMap的get,但是会根据accessOrder来调整链表维护的顺序,所以当accessOrder==true时,调用get,会修改链表维护的节点顺序,使得被访问节点移到链表尾部

基于LinkedHashMap实现LRU

1.前置知识

LRU(Least Recently Using):最近最少使用——一种CPU调度的算法

void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMap.Entry<K,V> first;
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
}
//主要是重载这个方法
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
}

2.实现

public class CacheHashMap<K, V> extends LinkedHashMap<K, V> {
    private int limit;

    public CacheHashMap(int limit) {
        super();
        this.limit = limit;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > limit;
    }
}
public class Main {
    public static void main(String[] args) {
        Map<String, Integer> map = new CacheHashMap<>(3);
        map.put("121233", 321);
        map.put("1asd22134", 321);
        map.put("12s51231", 321);
        System.out.println(map);

        map.put("ch", 123);
        System.out.println(map);
    }
}
/*
{121233=321, 1asd22134=321, 12s51231=321}
{1asd22134=321, 12s51231=321, ch=123}
*/

参考文献

LinkedHashMap源码详细分解(JDK1.8)

Stream

概述

理解

高级迭代器,只能遍历一次,要想操作流,首先需要一个数据源,可以是数组或集合,所以这里的Stream和I/O流没啥关系,反而和上面的Collection&Map有关,同时它也极大的方便了程序猿对集合的操作,极大的提高了编程效率和程序可读性。

特点

  • 不是数据结构,不会保存数据
  • 不会修改原来的数据源,它会将操作后的数据保存到另外一个对象中,方便进行链式操作
  • 惰性求值,流在中间处理过程中,只是对操作进行了记录,并不会立即执行,需要等到执行==终止操作==的时候才会进行实际的计算

分类

img

  • 无状态:指元素的处理不受之前元素的影响;
  • 有状态:指该操作只有拿到所有元素之后才能继续下去。
  • 非短路操作:指必须处理所有元素才能得到最终结果;
  • 短路操作:指遇到某些符合条件的元素就可以得到最终结果,如 A || B,只要A为true,则无需判断B的结果。

具体用法

创建流

public void createStream() throws FileNotFoundException {
        //通过集合创建
        List<String> list = new ArrayList<>();
        Stream<String> stringStream = list.stream();
        stringStream = list.parallelStream();

        //通过数组创建
        Integer[] nums = new Integer[]{1, 2, 3};
        Stream<Integer> integerStream = Arrays.stream(nums);
        integerStream.forEach(System.out::println);//1 2 3


        //通过Stream的静态方法创建
        Stream<String> stream = Stream.of("a", "b", "c", "d", "e");
        stream.forEach(System.out::println);//a b c d e
        Stream<String> stream1 = Stream.iterate("*", (x) -> x + 1).limit(2);
        stream1.forEach(System.out::println);//* *1
        Stream<Double> stream2 = Stream.generate(Math::random).limit(2);
        stream2.forEach(System.out::println);// 0.78135921935878 0.9577773304767536


        //使用BufferedReader.lines
        BufferedReader reader = new BufferedReader(new FileReader("XXX.txt"));
        Stream<String> lineStream = reader.lines();
        lineStream.forEach(System.out::println);//hhhh aaaa bbbb


        //Pattern.splitAsStream创建
        Pattern pattern = Pattern.compile(",");
        Stream<String> patternStream = pattern.splitAsStream("a,b,c");
        patternStream.forEach(System.out::println);//a b c
}

同时还可以发现一点,Stream.of(T... values)调用的其实是Arrays.stream()

public static<T> Stream<T> of(T... values) {
        return Arrays.stream(values);
}

//这个的源码还不是很懂,留坑好吧
public static<T> Stream<T> of(T t) {
        return StreamSupport.stream(new Streams.StreamBuilderImpl<>(t), false);
}

中间操作

筛选与切片

  • filter:过滤流中的元素
  • distinct:去重
  • skip(n):跳过n个元素
  • limit(n):获取n个元素
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6, 6, 7);
Stream<Integer> newStream = stream.filter(x -> x > 2).distinct().skip(1).limit(3);
newStream.forEach(System.out::println);//4 5 6

映射

  • map:接受一个函数参数,将旧元素映射成新元素
  • flatMap:接受一个函数参数,将旧元素映射成新元素,然后把所有流连接成一个流——”扁平化“操作
Stream<String> stream1 = Stream.of("a b c", "e f g");
Stream<String> newStream1 = stream1.map(x -> x.replaceAll(" ", ","));
newStream1.forEach(System.out::println);//a,b,c     e,f,g

Stream<String> stream2 = Stream.of("a b c", "e f g");
Stream<String> newStream2 = stream2.flatMap(x -> {
  String[] s = x.split(" ");
  Stream<String> ss = Stream.of(s);
  return ss;
});
newStream2.forEach(System.out::println);//a b c e f g

排序

  • sorted:自然排序
  • sorted(Comparator com):定制排序,自定义Comparator排序器
List<String> list = Arrays.asList("c", "a", "b");
list.stream().sorted().forEach(System.out::println);//a b c
list.stream().sorted((o1, o2) -> -o1.compareTo(o2)).forEach(System.out::println);//c b a

结束操作

匹配和聚合

  • allMatch:接收一个 Predicate 函数,当流中每个元素都符合该断言时才返回true,否则返回false
  • noneMatch:接收一个 Predicate 函数,当流中每个元素都不符合该断言时才返回true,否则返回false
  • anyMatch:接收一个 Predicate 函数,只要流中有一个元素满足该断言则返回true,否则返回false
  • findFirst:返回流中第一个元素,返回类型是Optional,详见下面Optional的解析,现在只用知道取元素使用orElse()
    • Optional findFirst();
  • findAny:返回流中的任意元素
    • Optional findAny();
  • count:返回流中元素的总个数,long类型
    • long count();
  • max:返回流中元素最大值
    • Optional max(Comparator<? super T> comparator);
  • min:返回流中元素最小值
    • Optional min(Comparator<? super T> comparator);
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
boolean flag = list.stream().allMatch(x -> x > 0);//true
flag = list.stream().anyMatch(x -> x < 0);//false
flag = list.stream().noneMatch(x -> x > 0);//false
Integer val = list.stream().findFirst().orElse(-1);//1
val = list.stream().findAny().orElse(-1);//1,但不一定
long cnt = list.stream().filter(x -> x > 3).count();//2
Integer maxx = list.stream().max(Integer::compareTo).orElse(-1);//5
Integer minn = list.stream().min(Integer::compareTo).orElse(-1);//1

规约

  • Optional reduce(BinaryOperator accumulator)
    • 第一次执行时,accumulator函数的第一个参数为流中的第一个元素,第二个参数为流中元素的第二个元素;第二次执行时,第一个参数为第一次函数执行的结果,第二个参数为流中的第三个元素,依次类推,即1 2 4,第一次1 + 2 = 3,第二次就是3 + 4 = 7
  • T reduce(T identity, BinaryOperator accumulator)
    • 流程跟上面一样,只是第一次执行时,accumulator函数的第一个参数为identity,而第二个参数为流中的第一个元素,即1 2 4,identity = 5,第一次5 + 1 = 6,第二次6 + 2 = 8,第三次8 + 4 = 12
  • U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator combiner)
  • 在串行流(stream)中,该方法跟第二个方法一样,即第三个参数combiner不会起作用
  • 在并行流(parallelStream)中,我们知道流被fork join出多个线程进行执行,此时每个线程的执行流程就跟第二个方法reduce(identity,accumulator)一样,而第三个参数combiner函数,则是将每个线程的执行结果当成一个新的流,然后使用第一个方法reduce(accumulator)流程进行规约
List<Integer> integerList = Arrays.asList(1, 2, 3, 4);
Integer value = integerList.stream().reduce((x, y) -> x * y).orElse(-1);//24
Integer value1 = integerList.stream().reduce(3, (x, y) -> x * y);//72
Integer value2 = integerList.stream().reduce(3, (x, y) -> x * y, (x, y) -> x - y);//72
//经过别人的测试,当元素个数小于24时,并行时线程数等于元素个数,当大于等于24时,并行时线程数为16
//这里我的理解是每个元素是一个线程,所以每个线程和3相乘,再返回结果进行combiner函数组合在一起,这里是全部加起来
Integer value3 = integerList.parallelStream().reduce(3, (x, y) -> x * y, (x, y) -> x + y);//30 = 1 * 3 + 2 * 3 + 3 * 3 + 4 * 3 

收集

collect:接收一个Collector实例,并将流中元素收集成另外一个数据结构

Collector类总共有5个接口(==还不是很懂,有时间可以去学一下函数式接口==)

Student student = new Student("aa", 1);
Student student1 = new Student("bb", 1);
Student student2 = new Student("cc", 3);
List<Student> list = Arrays.asList(student, student1, student2);
List<Integer> integerList = list.stream().map(Student::getAge).collect(Collectors.toList());//[1, 1, 3]
Set<Integer> integerSet = list.stream().map(Student::getAge).collect(Collectors.toSet());//[1, 3]
Map<String, Integer> stringIntegerMap = list.stream().collect(Collectors.toMap(Student::getName, Student::getAge));//{cc=3, bb=1, aa=1}
String str = list.stream().map(Student::getName).collect(Collectors.joining(",", "(", ")"));//(aa,bb,cc)

Optional

图解Optional是什么???

可以避免没有必要的 null 值检查

img

具体用法

创建Optional

使用静态方法 ofNullable() 创建一个即可空又可非空的 Optional 对象

String str = null;
Optional<String> optionalS = Optional.ofNullable(str);
System.out.println(optionalS);//Optional.empty

判断值是否存在

通过方法 isPresent() 判断一个 Optional 对象是否存在,如果存在,该方法返回 true,否则返回 false

System.out.println(optionalS.isPresent());//false

非空表达式

ifPresent判断一个 Optional 对象是否存在,如果存在,就执行后面的方法

Optional<String> optional = Optional.ofNullable("123");
optional.ifPresent(System.out::println);

设置(获取)默认值

orElse() | orElseGet()

Optional<String> optional = Optional.ofNullable(null);
System.out.println(optional.orElse("123"));//123
System.out.println(optional.orElseGet(() -> "123"));//123

//orElseGet()的参数是函数式接口,所以需要传入lambda
public T orElseGet(Supplier<? extends T> other) {
        return value != null ? value : other.get();
}

从性能上来讲,orElseGet()性能更佳,因为在Optional对象不为null的时候,orElse会执行,而orElseGet()不会执行

所以建议使用orElseGet()

补充

函数式接口(留图再留坑)

img

数值流

Java 8引入了intStream,DoubleStream,LongStream,分别将流中的元素特化成int,double,long,从而避免装箱操作。特化的原因是因为装箱造成的复杂性,即int和Integer的效率差异

  • 映射到数据流:mapToInt,mapToDouble,mapToLong,返回特化流,而不是Stream<T>
  • 转化为对象流:boxed可以将数值流特化为对象流
  • 默认值OptionalInt,Optional的特化版本也是OptionalInt....

数值流应用:勾股数

Stream<int[]> pythagoreanTriples = IntStream.rangeClosed(1, 100).boxed()
          .flatMap(a -> IntStream.rangeClosed(a, 100)
                   .filter(b -> Math.sqrt(a * a + b * b) % 1 == 0).boxed()
                   .map(b -> new int[]{a, b, (int) Math.sqrt(a * a + b * b)})
                  );
pythagoreanTriples.forEach(t -> System.out.println(Arrays.toString(t)));

参考文献

Java 8 stream的详细用法

Java 8 Optional 最佳指南

Java8特性③Stream的使用

微信图片_20200921212827.jpg

I/O类

装饰者模式

Java的IO类的内容感觉很多,但本质是装饰者模式,所以想学习IO类,得先对装饰者模式有个基本的了解

image-20201102111536649

之前用C++写过一次装饰者模式,今天换成java试一下

public interface Human {//基类
    void talk();

    void wear();
}
public abstract class Decorator implements Human {//装饰者抽象类
    private Human human;

    public Decorator(Human human) {
        this.human = human;
    }

    public void talk() {
        human.talk();
    }

    public void wear() {
        human.wear();
    }
}
public class Decorator_1 extends Decorator {//装饰者具体类1

    public Decorator_1(Human human) {
        super(human);
    }

    @Override
    public void talk() {
        super.talk();
        System.out.println("装饰者1:talk");
    }

    @Override
    public void wear() {
        super.wear();
        System.out.println("装饰者1:wear");
    }
}
public class Decorator_2 extends Decorator{//装饰者具体类2

    public Decorator_2(Human human) {
        super(human);
    }

    @Override
    public void talk() {
        super.talk();
        System.out.println("装饰者2:talk");
    }

    @Override
    public void wear() {
        super.wear();
        System.out.println("装饰者2:wear");
    }
}
public class Person implements Human {//被装饰者具体类
    @Override
    public void talk() {
        System.out.println("talk");
    }

    @Override
    public void wear() {
        System.out.println("wear");
    }
}
public class Main {//测试类
    public static void main(String[] args) {
        Human human = new Person();
        Decorator decorator = new Decorator_1(new Decorator_2(human));
        decorator.talk();
        decorator.wear();
    }
}
/*
talk
装饰者2:talk
装饰者1:talk
wear
装饰者2:wear
装饰者1:wear
*/

流的基本概念

流的本质

数据传输,根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作

IO分类

输入/输出

  • 输入流:输入到内存
    • InputStream/Reader
  • 输出流:从内存输出
    • OutputStream/Writer

这里写图片描述

这幅图其实有个小问题,运行中的程序,我们应该叫做进程,程序本身是静态的,不具有堆栈这些的内存空间

字节流/字符流

  • 字节和字符的区别,1字节=8位,1字符=16位(java中)
  • 字节流不会使用到缓冲区,字符流会使用到缓冲区,通过缓冲区操作文件

img

在java中,char ch='恒';是允许的,因为在java中一个char占两个字节,为了表示中文,采用的是Unicode编码

  • 字节流:有中文会乱码
    • InputStream/OuputStream
  • 字符流:正常显示中文
    • Reader/Writer

节点流/处理流

  • 节点流:特定源和终端的读写
    • 例如FileInputStream
  • 处理流:对已存在的流进行封装,通过所封装的流的功能调用实现数据读
    • 处理流的构造方法总是要带一个其他的流对象做参数
    • 例如BufferedReader

常用流分类表(图)

分类字节输入流字节输出流字符输入流字符输出流
抽象基类InputStreamOutputStreamReaderWriter
访问文件FileInputStreamFileOutputStreamFileReaderFileWriter
访问数组ByteArrayInputStreamByteArrayOutputStreamCharArrayReaderCharArrayWriter
访问管道PipedInputStreamPipedOutputStreamPipedReaderPipedWriter
访问字符串 StringReaderStringWriter
缓冲流BufferedInputStreamBufferedOutputStreamBufferedReaderBufferedWriter
转换流 InputStreamReaderOutputStreamWriter
对象流ObjectInputStreamObjectOutputStream
抽象基类FilterInputStreamFilterOutputStreamFilterReaderFilterWriter
打印流 PrintStream PrintWriter
推回输入流PushbackInputStream PushbackReader
特殊流DataInputStreamDataOutputStream

==表中粗体字所标出的类代表节点流,必须直接与指定的物理节点关联:斜体字标出的类代表抽象基类,无法直接创建实例==

img

具体用法

带资源的try

我们在使用I/O类或网络连接的时候,我们经常会忘记关闭,或编码麻烦,所以在JDK1.7推出了带资源的try语句,try语句在该语句结束时自动关闭这些资源,这些资源必须实现AutoCloseable或者Closeable接口,实现这两个接口就必须实现close() 方法

public class TryTest {
    public static void main(String[] args) {
      	//try()就是带资源try的用法了,还是很简单的
        try (FileReader fis = new FileReader("in.txt");
             FileWriter fos = new FileWriter("out.txt")) {
            char[] b = new char[1024];
            int len = fis.read(b);
            fos.write(b, 0, len);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

文件流的使用

FileInputStream/FileOutputStream

public void fileStreamTest() throws IOException {
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            fis = new FileInputStream("in.txt");
            fos = new FileOutputStream("out.txt");
            byte[] b = new byte[1024];
            int hasRead = 0;
            while ((hasRead = fis.read(b)) > 0) {
                fos.write(b, 0, hasRead);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            fis.close();
            fos.close();
        }
}

FileReader/FileWriter

public void fileTest() throws IOException {
        FileReader fr = null;
        FileWriter fw = null;
        try {
            fr = new FileReader("in.txt");
            fw = new FileWriter("out.txt");
            char[] b = new char[1024];
            int hasRead = 0;
            while ((hasRead = fr.read(b)) > 0) {
                fw.write(b, 0, hasRead);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            fr.close();
            fw.close();
        }
}

两个的用法其实差不多,然后我发现一点超神奇的东西,就是FileReader的构造函数使用的FileInputStream的构造函数

public FileReader(String fileName) throws FileNotFoundException {
        super(new FileInputStream(fileName));
}

缓冲流的使用

先来介绍一下这个缓冲流,缓冲流就是之前介绍的处理流,也是我们在装饰者模式中介绍到的装饰者这个角色,它继承装饰者的基类FilterInputStream/FilterOutputStream,而被装饰者则是FiltInputStream/FileOutputStream这些节点流

BufferedInputStream/BufferedOutputStream

public void fileStreamTest() throws IOException {
        FileInputStream fis;
        FileOutputStream fos;
        BufferedInputStream bis = null;
        BufferedOutputStream bos = null;
        try {
            fis = new FileInputStream("in.txt");
            fos = new FileOutputStream("out.txt");
            bis = new BufferedInputStream(fis);
            bos = new BufferedOutputStream(fos);
            byte[] b = new byte[1024];
            int hasRead = 0;
            while ((hasRead = bis.read(b)) > 0) {
                bos.write(b, 0, hasRead);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            bis.close();
            bos.close();
        }
}

代码方面其实没差多少,就是把FileInputStream,FileOutputStream嵌入到BufferedInputStream和BufferedOutputStream中

我们主要讲讲缓冲流的作用,它可以提高读写速度,减少IO操作,你可以试想一下你的饭碗,你是不是乘一碗饭来吃,为什么不吃一口乘一口,原因不就是因为用碗吃饭快嘛,其实呢,这个碗就是是个缓冲区,你把数据都先读取到缓冲区中,而缓冲区在内存中,内存读写速度快,同时这也减少了对电饭煲(IO)的访问。默认的缓冲区大小是8192 char/byte

==讲到这里我其实就有一个疑问🤔️,字符流不是已经使用了缓冲区了嘛,那再来一个缓冲流,这两个缓冲之间又是什么关系呢???就很多细节还不是很明白,还是太菜了!!!估计得深入底层才能知道这些答案了,加油!==

BufferedReader/BufferedWriter

这里也不赘述了,其实跟上面没差多少

需要注意一个小点,BufferedReader有readLine这个方法,但相应的BufferedInputStream中就没有,我猜测可能是因为字节中没有行这个语义,而字符中有,试想一下什么二进制文本,你知道这一行是干嘛的嘛,不知道吧,所以也就没有提供读取一行的方法.....(只是猜测而已哈哈哈)

转化流的使用

InputStreamReader/OutputStreamWriter

public void inputStreamReaderTest() throws IOException {
        InputStreamReader isr = new InputStreamReader(System.in);
        BufferedReader br = new BufferedReader(isr);
        String str;
        while ((str = br.readLine()) != null) {
            System.out.println(str);
        }
}

//System中的in属于InputStream类
public final static InputStream in = null;

//InputStreamReader类属于Reader类
public class InputStreamReader extends Reader {
  ....
}

我们可以看到我们把System.in从InputStream类转化为Reader类,再通过BufferedReader装饰,再读取控制台输入,标准输出打印

==没有找到字符流转字节流的方法,所以再次留坑,太菜了,吐血,网上好多人都挂羊头卖狗肉!!!==

对象流的使用

对于对象流的了解,我目前只停留在对象的深拷贝和能使对象脱离进程独立存在于文件或其他媒介中的水平,所以实例也是对象的深拷贝了

public class CloneUtil {
  		//<T extends Serializable>定义了方式方法的上界,必须继承或实现某个类或接口
      public static <T extends Serializable> T clone(T obj) {
          T cloneObj = null;
          try {
              ByteArrayOutputStream baos = new ByteArrayOutputStream();//先获取数组流,因为是克隆,所以也不需要源
              ObjectOutputStream objos = new ObjectOutputStream(baos);//装饰者,包装成对象流
              objos.writeObject(obj);//写入传入的对象

              ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());//获取刚才写的对象
              ObjectInputStream objis = new ObjectInputStream(bais);//包装
              cloneObj = (T) objis.readObject();//读取对象
          } catch (IOException | ClassNotFoundException e) {
              e.printStackTrace();
          }
          return cloneObj;//返回克隆的对象
      }
}

class Person implements Serializable {
    StringBuilder name;
    int age;
  	//Setter/Getter/toString/(有/无)参构造函数....
}
//测试类
public class Main {
      public static void main(String[] args) {
          Person person = new Person(new StringBuilder("ch"), 20);
          Person clonePerson = CloneUtil.clone(person);
          clonePerson.getName().append("hhhh");
        
          System.out.println(person);
          System.out.println(clonePerson);
      }
}
/*
Person{name='ch', age=20}
Person{name='chhhhh', age=20}
*/
//检测成功!!!

参考文献

java IO体系的学习总结

死磕Java基础--Java中的I/O流,看这个就够了!

Java利用序列化实现对象的深拷贝

# 入门  Java 

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×