Java编程思想(二)

数组

// 多维数组中构成矩阵的每个向量都可以具有任意长度,也称为粗糙数组
// 只有第一维必须需要指定长度(如果不指定其他维度长度,相当于仍然为一维数组,且所有元素都是null)
// 多维数组从第二维开始都是对象引用,可以为null
Random random = new Random(47);
int[][][] ints = new int[random.nextInt(7)][][];

// 元素为List<String>的数组
List<String>[] ls;

// 数组是协变的;集合不是
String[] strings = new String[10];
Object[] objects = strings;
// List<String> strings = new ArrayList<>();
// List<Object> objects = objects;
  1. 数组与其他种类容器之间的区别有三个方面:效率类型保存基本数据类型
  2. 数组是一种效率最高的存储和随机访问对象引用序列的方式
  3. 线性序列 ==> 随机访问非常快
  4. 大小(长度)被固定,且在其生命周期内不可变
  5. ArrayList虽然支持弹性扩容,但这种弹性需要开销,因此ArrayList的效率比数组低很多
  6. 数组标识符其实只是一个引用,指向在堆中创建的一个真实对象,这个(数组)对象用以保存指向其他对象的引用
  7. 对象数组保存的是引用;基本类型数组直接保存基本类型的值
  8. char数组的默认值是'\u0000' 0存疑:IDEA中拷贝出来的;JDK11.11
  9. 多维数组中构成矩阵的每个向量都可以具有任意长度,也称为粗糙数组
  10. 数组必须知道它们所持有的确切类型,以强制保证类型安全
  11. 优先使用泛型方法而不是泛型类

容器深入研究

在这里插入图片描述

此图为Java编程思想第四版插图(可能与JDK8+版本实现有所不同;阅读此书籍时,本地JDK为11.11)

  • JDK11.11+中:TreeMap<K,V> implements NavigableMap<K,V> &
    NavigableMap<K,V> extends SortedMap<K,V>

  • Set

    • LinkedHashSet维护的是保持了插入顺序的链接列表
    • TreeSetSortedSet的唯一实现(按照元素的比较函数对元素排序)
    • 没有重新定义hashCode()的类放置到任何散列中都可能会产生重复(建议同时实现equals()hashCode()
    • HashSet的性能基本上总是比TreeSet好,特别是在添加和查询元素时。TreeSet存在的唯一原因就是它可以维持元素的排序状态。因为其内部结构支持排序,用TreeSet迭代通常比HashSet
  • Queue是队列,Deque是双端队列

    • Queue是一种常用的数据结构,可以将队列看做是一种特殊的线性表,该结构遵循的先进先出原则
      LinkedList实现了Queue接口,因为LinkedList进行插入、删除操作效率较高)
    • DequeQueue的一个子接口;该队列两端的元素既能入队(offer)也能出队(poll)
      (如果将Deque限制为只能从一端入队和出队,则可实现栈的数据结构)
      (对于而言,有入栈(push)和出栈(pop),遵循先进后出原则)
    • LinkedList<E> implements Deque<E> & Deque<E> extends Queue<E>
    • Deque不如Queue常用
  • Map

    • Map(映射表)的性能是一个很重要的问题;HashMap中使用了散列码来取代键的缓慢搜索
    • 散列是映射中存储元素时最常用的方式
    • LinkedHashMap可以在构造器中设定是否使用最近最少使用 LRU算法
    • 使用LRU,没有被访问过的元素会出现在队列的前面(适用于需要定期清理元素场景)
    • 默认为按照插入顺序
    • 默认情况下以插入顺序LRU序循环时都以插入顺序进行遍历;访问到一定数量的元素后,LRU模式获取元素的顺序才会发生改变
    • 使用自定义的类作为键时(如MapSet),必须同时重载hashCode()equals()
      hashCode()并不需要总是能够返回唯一的标识符,但equals()方法必须严格判断两个对象是否相同
    • Map接口entrySet()的恰当实现应该在Map中提供视图,而不是副本,并且这个视图允许对原始映射表进行修改
  • equals()方法实现必须满足以下条件

    • 自反性。对任意xx.equals(x)一定返回true
    • 对称性。对任意xy,如果y.equals(x)返回true,则x.equals(y)也返回true
    • 传递性。对任意x y z,如果有x.equals(y)返回truey.equals(z)返回true,则x.equals(z)一定返回true
    • 一致性。对任意x y,如果对象中用于等价比较的信息没有改变,那么无论调用x.equals(y)多少次,返回的结果应该保持一致,要么一直是true,要么一直是false
    • 对任何不是nullxx.equals(null)一定返回false
  • 散列的价值在于速度

    HashMap保存数据机制:

    1. 散列将键保存在某处
    2. 存储一组元素最快的数据结构是数组,因此使用它来表示键的信息
    3. (数组容量不可调整)数组并不保存键本身。而是通过键对象生成一个数字,并将其作为数组的下标,这个数字就是散列码,由定义在Object中且可覆盖的hashCode() 散列函数方法生成
    4. 为解决数组容量固定的问题,不同的键可以产生相同的下标,即可能会产生冲突。因此数组多大就不重要了,任何键总能在数组中找到自己的位置
    5. 生成桶的下标前,hashCode()还需要进一步处理,所以散列码的生成范围并不重要,只要是int即可
    6. 好的hashCode()应该产生分布均匀的散列码(分布不均可能导致某些区域负载过重)

    散列码不必是独一无二的(应该更关注生成速度,而不是唯一性),但是通过hashCode()equals()必须能够完全确定对象的身份

  • 快速报错fast-fail是java集合(Collection)中的一种错误机制。多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件;只能被用来检测错误,因为JDK并不保证fail-fast机制一定会发生

    1. 集合修改次数参数modCount
    2. 所有涉及修改集合中元素个数的操作,都会改变modCount的值
    3. 迭代器Iterator持有参数expectedModCount,默认expectedModCount = modCount
    4. 遍历过程中若expectedModCount != modCount,抛出异常ConcurrentModificationException,即产生fast-fail
  • 持有引用

    • 当存在可能会耗尽大量内存的大对象时,这些类显得特别有用
    • 三个继承自Reference的类:SoftReferenceWeakReferencePhantomReference。当垃圾回收器正在考察的对象只能通过某个Reference对象才“可获得”时,这三个类为垃圾回收器提供了不同级别的间接性指示
    • 如果希望一个对象能够被访问,同时希望能够允许垃圾回收器释放它,就可以使用Reference对象
      (可以继续使用该对象,当内存消耗殆尽时又允许回收此对象)
      条件:一定不能有普通的引用指向该对象(普通引用指没有经Reference对象包装过的引用)
    • SoftReferenceWeakReferencePhantomReference由强到弱排列,对应不级别的“可获得性”

Java I/O 系统

类结构图:

在这里插入图片描述

// 视图缓冲器
// 真实定义 ByteBuffer
ByteBuffer bb = ByteBuffer.wrap(new byte[]{0, 0, 0, 0, 0, 0, 0, 'a'});
bb.rewind();
// 获取其他视图
CharBuffer cb = bb.asCharBuffer();
FloatBuffer fb = bb.asFloatBuffer();
IntBuffer ib = bb.asIntBuffer();
LongBuffer lb = bb.asLongBuffer();
ShortBuffer sb = bb.asShortBuffer();
DoubleBuffer db = bb.asDoubleBuffer();
  • Java I/O设计中大量使用了装饰器设计模式

  • RandomAccessFile适用于由大小已知的记录组成的文件。与InputStreamOutputStream无任何关系,是一个完全独立的类,拥有和别的I/O类型本质不同的行为(可以在一个文件内向前或向后移动),直接从Object派生而来

  • 管道流用于任务之间的通信

  • 缓冲往往能显著的增加I/O操作的性能

  • 旧的I/O包已经使用NIO重新实现过(即便不显示的使用NIO编写代码,也能从中受益)
    (速度的提升来自于所使用的结构更接近于操作系统执行I/O的方式:通道和缓冲器

    我们可以把它想象成一个煤矿,通道是一个包含煤层(数据)的矿藏,而缓冲器则是派送到矿藏的卡车。卡车满载而归,我们再从卡车上获得煤炭。
    即不直接与通道交互,只和缓冲器交互,并把缓冲器再派送到通道。
    通道要么从缓冲器获得数据,要么向缓冲器发送数据

  • 缓冲器容纳的是普通的字节,为了把它们转换成字符,我们要么在输入它们时进行编码,要么在将其从缓冲器输出时对它们进行解码

  • 视图缓冲器可以让我们通过某个特定的基本数据类型的视窗查看其底层的ByteBuffer
    ByteBuffer依然是实际存储数据的地方,对视图的任何修改都会映射为对ByteBuffer的操作)
    各种不同的视图数据显示的方式也不相同:
    在这里插入图片描述

  • 字节存放次序

    • big endian:高位优先。将最重要的字节存放在地址最低的存储器单元
    • little endian:低位优先。将最重要的字节存放在地址最高的存储器单元
    • ByteBuffer默认是以big endian 高位优先的形式存储数据的(网络传输时也多以高位优先
    • ByteOrder.BIG_ENDIAN & ByteOrder.LITTLE_ENDIAN
  • ByteBuffer是将数据移进移出通道的唯一方式,并且只能创建一个独立的基本类型缓冲器,或者使用as方法从ByteBuffer中获得

在这里插入图片描述

NIO类关系(Java编程思想第四版)

  • 文件锁

  • Java的对象序列化将那些实现了Serializable接口的对象转换成一个字节序列,并能够在以后将这个字节序列完全恢复为原来的对象

  • 对象序列化主要为了支持两种特性:

    1. Java的远程方法调用(RMI),使存活于其他计算机上的对象使用起来就像存活于本机上一样(需要通过序列化传出参数及返回值)
    2. 对Java Beans来说序列化也是必须的
  • 对象序列化是基于字节的

  • Serializable & Externalizable 区别

    1. Serializable对象完全以存储的二进制位为基础来构造,不调用构造器
    2. Externalizable对象,默认只会调用源对象的默认无参构造(必须存在)进行(反)序列化【使用方法writeExternal()&&readExternal()进行定制】
  • 对于实现了Serializable接口的类,使用transient逐字段关闭(反)序列化

    使用transient修饰的字段并非能完全关闭(反)序列化

  • Externalizable的替代方法

    实现Serializable接口,并“添加”方法writeObject()readObject();(反)序列化会自动调用(必须具有准确的方法签名)
    【对象 ObjectOutputStream || ObjectInputStream 的对应方法执行调用】
    方法定义是 private 的,不是接口方法却可以被正常调用

枚举

// 一个简单的 Reflection.java 反编译结果
// 1、类是final的;2、实例是static final的;3、新增了一个static方法
Compiled from "Reflection.java"
final class cn.yangcx.enum19.Explore extends java.lang.Enum<cn.yangcx.enum19.Explore> {
  public static final cn.yangcx.enum19.Explore HERE;
  public static final cn.yangcx.enum19.Explore THERE;
  public static cn.yangcx.enum19.Explore[] values();
  public static cn.yangcx.enum19.Explore valueOf(java.lang.String);
  static {};
}

// 枚举子类化
public interface Food{
    enum Appetizer implements Food{;}
    enum MainCourse implements Food{;}
    enum Dessert implements Food{;}
    enum Coffee implements Food{;}
}

// enum编写方法
public enum ConstantSpecificMethod {
    // 枚举对象实现方法
    DATE_TIME {
        String getInfo() {
            return DateFormat.getDateInstance().format(new Date());
        }
    };
    // 定义方法(普通方法或抽象方法都行)
    abstract String getInfo();
}
  • 所有的枚举类都继承自java.lang.Enum

  • ordinal()方法每个enum声明时的次序,从0开始

  • 编译器自动提供equals()hashCode()方法

  • Enum实现了java.lang.Comparable接口

  • 枚举中任意方法定义必须在enum实例之后

  • 枚举类的构造器是否为private关系不大(只能在enum定义的内部通过构造器创建实例)

  • 如果在switch case中调用return,编译器可能会警告没有default(确实没有default的情况)
    与是否使用枚举类型无关

  • 编译器自动为创建的枚举类新增两个静态方法:values()valueOf()
    java.lang.EnumvalueOf()方法有两个参数)

  • 反编译后,枚举类及enum实例都是final的;且多了一个static方法

  • 对于enum而言,实现接口是使其子类化的唯一办法

  • enum要求其成员必须是唯一的

  • EnumSet性能非常好(可能是将一个long值作为比特向量;具体原理及作用也搞不清楚

  • EnumMap内部由数组实现,所以速度飞快

    enum实例的定义顺序决定了其在EnumSetEnumMap中的顺序
    EnumMap允许开发者修改对象

  • 存在多种enum,且他们之间存在互操作的情况下,可以使用EnumMap实现多路分发(multiple dispatching)

  • Javaenum允许开发者为每个enum实例编写方法;表驱动的代码(table-driven code)

注解

// 注解定义
// 如果希望此注解应用于所有的 ElementType,可以省去 @Target(很少见)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
    // 元素不是必须的
	String description() default "no description";
    // 元素不能由不确定的值;不能以 null 作为其值(如果需要,可以使用 -1 或空字符串替代)
    // String desc() default null;
}
  • 注解定义
    • @Target:此注解应用于什么地方(METHODFIELD。。。)
      (如果希望此注解应用于所有的ElementType,可以省去 @Target(很少见))
    • @Retention:此注解在哪一个级别可用(SOURCE源代码、CLASS类文件、RUNTIME运行时。。。)
    • @Documented:此注解将包含在Javadoc
    • @Inherited:允许子类继承父类中的注解
  • 没有元素的注解被称为标记注解
  • 注解可以嵌套
  • 元素不能有不确定的值;也不能以null作为其值
  • 使用多个注解时,同一个注解不能重复使用
  • 注解不支持继承(不能使用extends语法)

并发

// 第一个线程设置为守护线程并启动任务中的线程
Thread thread = new Thread(new Daemon());
thread.setDaemon(true);
thread.start();
// 子线程 Daemon.java
private final Thread[] threads = new Thread[10];
@Override
public void run() {
    for (int i = 0; i < threads.length; i++) {
        //调用当前线程的线程是一个守护线程,导致当前线程也是守护线程
        threads[i] = new Thread(new DaemonSpawn());
        threads[i].start();
    }
    for (int i = 0; i < threads.length; i++) {
        // 这里全部都是 isDaemon = true
        print("t[" + i + "].isDaemon() = " + threads[i].isDaemon() + ",");
    }
}

// 无法从线程中捕获异常
public class ExceptionThread implements Runnable {
    @Override
    public void run() {
        // 直接抛出异常
        throw new RuntimeException();
    }
    public static void main(String[] args) {
        try {
            ExecutorService exec = Executors.newCachedThreadPool();
            exec.execute(new ExceptionThread());
        } catch (RuntimeException ue) {
            // todo 这一句不会执行
            System.out.println("Exception has been handled!");
        }
    }
}

// 捕获线程抛出异常
class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
    // todo 实现方法,处理异常
}
// 设置未捕获异常处理器
thread.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
// OR 设置默认异常处理器
Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());

// CountDownLatch
// 计数
final CountDownLatch countDownLatch = new CountDownLatch(2);
// 阻塞等待其他线程完成;计数归零后自动唤醒继续执行
countDownLatch.await();
// 计数 -1
countDownLatch.countDown();

// CyclicBarrier
// 屏障个数;barrierAction-屏障满足条件后优先执行的方法
CyclicBarrier cyclicBarrier = new CyclicBarrier(3, null);
// 到达屏障后阻塞等待;满足条件后优先执行barrierAction(如果存在),再继续执行后续方法
cyclicBarrier.await();

// Semaphore
// fair – true if this semaphore will guarantee first-in first-out granting of permits under contention, else false
Semaphore semaphore = new Semaphore(size, true);
// 请求许可(阻塞等待)
available.acquire();
// 返还许可
available.release();
  • 实现线程的两种方式
    • myThread extends Thread
    • myRunnable implements Runnable
  • 通过多线程机制,子任务中的每一个都将由执行线程来驱动;其底层机制是切分CPU时间
    (CPU将轮流给每个任务分配其占用时间)
  • ExecutorJava SE5/6中启动任务的优选方法
  • 在任何线程池中,现有线程在可能的情况下,都会被自动复用
  • 如果希望从任务在完成时能够返回一个值,可以实现Callable接口
  • 调度器倾向于让优先级越高的线程先执行(优先级越低的线程也会执行,只是执行的频率较低)
    (绝大多数情况下,所有线程都应该按照默认的优先级运行。不建议操作线程优先级)
  • 尽管JDK有 10 个优先级,但它与多数操作系统都不能映射的很好
  • Thread.yield()可以建议CPU时间片从一个线程转移到另一个线程(不保证一定生效)
    yield()sleep():线程并不会释放锁
    wait():线程会释放锁
  • 通过线程Thread0(此线程为守护线程)启动多个子线程ThreadN(不显式设置为守护线程),那么子线程也会自动的成为守护线程
  • 使用原生的Thread并设置为守护线程时,任务中的finally语句块不会执行
    (非守护线程时,finally语句块会正常执行 ==> main主线程结束后,系统不会以一种优雅的方式退出守护线程
  • Thread类自身并不执行任何操作,它只是驱动赋予它的任务
  • 一个线程可以在其他线程之上调用join()方法,其效果是等待一段时间直到第二个线程结束才继续执行
    例:如果在线程t1上执行t2.join(),线程t1将被挂起,直到线程t2结束才恢复
  • 无法从线程中捕获异常(使用try-catch不能捕获,需要使用其他方式实现)
  • 捕获从线程抛出的异常:Thread.UncaughtExceptionHandler
  • 原子操作是不能被线程调度机制中断的操作,一旦开始,一定可以在切换到其他线程之前完成操作
    (依赖于原子性是很棘手且很危险的,问题:原子操作是否需要进行同步控制?
  • volatile确保了域在应用中的跨线程的可视性;同步synchronized也会导致向主存中刷新
    (如果一个域完全由synchronized方法或语句块来防护,不必将其设置为volatile的)
  • volatile会移除编译器所有关于读取和写入操作的优化
  • 相比于使用volatile关键字(使用起来需要考虑的更周全),使用更安全(synchronizedLock对象)
  • synchronized不属于方法签名
  • 建议使用同步代码块(synchronized修饰方法内的部分代码)而不对整个方法进行同步
  • 为使用相同变量的每个不同的线程创建不同的存储,即线程本地存储,可由java.lang.ThreadLocal实现
  • 线程状态及转换
    线程状态及转换
  • 线程中断
    • run()中使用return
    • 调用Thread.interrupted()(新的concurrent库阻止了对此方法的直接调用)
    • 调用ExecutorService.shutDown() || ExecutorService.shutDownNow()关闭所有任务
    • 调用ExecutorService.sumit()方法返回对象Future<?>.cancel(boolean mayInterruptIfRunning)关闭单个任务
  • 能够中断对sleep()的调用,但无法中断试图获取synchronized锁或任何试图执行I/O操作的线程
    I/O操作可以通过关闭底层资源进行中断,不是必需操作
  • 只能在同步控制方法或同步控制块(synchronized)里调用wait()notify()notifyAll()
  • 使用notify()而不是notifyAll()是一种优化(只会唤醒等待这个锁的相关任务)
  • Lock && Condition:每个对lock()的调用必需使用try-catch子句保证锁的正常释放(lock()try之前执行);任务在调用await()singal()singalAll()之前必需先获取锁
    LockCondition对象只有在更困难的多线程问题中才是必需的)
  • CountDownLatch:一个或者多个线程,等待其他多个线程完成某件事情之后才能执行

    A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes

    • 用给定的计数N初始化CountDownLatch
    • 使用countDown()使计数归零之前,await()方法会一直阻塞。归零之后,释放所有等待的线程,await()的所有后续调用都将立即返回
    • 计数无法被重置
    • 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行
  • CyclicBarrier字面意思为可循环使用(Cyclic)的屏障(Barrier)。让一组线程到达一个同步点后再一起继续运行,在其中任意一个线程未达到同步点,其他到达的线程均会被阻塞

    A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point

    • (构造函数CyclicBarrier(int parties, Runnable barrierAction)也可以实现所有await()满足后优先执行barrierAction
  • CountDownLatchCyclicBarrier对比
    • 对于CountDownLatch来说,重点是“一个或多个线程等待”,而其他N个线程在完成“某件事情”后可以终止也可以等待;
    • 对于CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须等待
    • CountDownLatch是计数器,线程完成一个记录一个,只不过计数不是递增而是递减
    • CyclicBarrier更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行
    • CountDownLatch简单的说就是一个线程等待,直到他所等待的其他线程都执行完成并且调用countDown()方法发出通知后,当前线程才可以继续执行
    • CyclicBarrier是所有线程都进行等待,直到所有线程都准备好进入await()方法之后,所有线程同时开始执行!
      (构造函数CyclicBarrier(int parties, Runnable barrierAction)也可以实现所有await()满足后优先执行barrierAction
    • CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset()方法重置。
      (所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次)
    • CyclicBarrier还提供其他有用的方法
      (比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断)
  • Semaphore:计数信号量,允许n个任务同时访问某个资源
  • java.util.concurrent.Exchanger类可用于两个线程之间交换信息

    可简单地将Exchanger对象理解为一个包含两个格子的容器,通过exchange方法可以向两个格子中填充信息。
    当两个格子中均被填充时,该对象会自动将两个格子的信息交换,然后返回给线程,从而实现两个线程的信息交换

    • Exchanger类仅可用作两个线程的信息交换
    • 超过两个线程调用同一个exchanger对象时,得到的结果是随机的(并且只有两个线程能交换成功,未配对的线程会被阻塞,永久等待)
  • SynchronizedLock对比
    • 使用Lock通常会比使用synchronized高效的多,而且synchronized的开销变化范围更大,而Lock相对更一致
    • synchronized的代码可读性更高
    • 更安全的做法是以更加传统的互斥方式入手,只有在对性能有明确需求时再替换为Lock
  • 免锁容器:
    • CopyOnWriteArrayListCopyOnWriteArraySetCopyOnWriteArraySet底层使用CopyOnWriteArrayList承载数据);写入操作会导致创建整个底层数组的副本
    • ConcurrentHashMapConcurrentLinkedQueue允许并发读取和写入;写入操作只会导致容器中的部分内容(分段)发生复制
  • ReadWriteLock对向数据结构相对不频繁的写入,但是有多个任务经常读取这个数据结构的这类情况进行了优化
    • 可以拥有多个读取者,只要他们都不试图写入即可
    • 如果写锁被其他线程持有,那么任何读取者都不能访问,直到写锁被释放

参考资料:

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值