mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-17 16:56:40 +08:00
1923 lines
46 KiB
HTML
1923 lines
46 KiB
HTML
<!DOCTYPE html>
|
||
|
||
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
|
||
|
||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||
|
||
<head>
|
||
|
||
<head>
|
||
|
||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
|
||
|
||
<link rel="icon" href="/static/favicon.png">
|
||
|
||
<title>72 final 的三种用法是什么?.md.html</title>
|
||
|
||
<!-- Spectre.css framework -->
|
||
|
||
<link rel="stylesheet" href="/static/index.css">
|
||
|
||
<!-- theme css & js -->
|
||
|
||
<meta name="generator" content="Hexo 4.2.0">
|
||
|
||
</head>
|
||
|
||
|
||
|
||
<body>
|
||
|
||
|
||
|
||
<div class="book-container">
|
||
|
||
<div class="book-sidebar">
|
||
|
||
<div class="book-brand">
|
||
|
||
<a href="/">
|
||
|
||
<img src="/static/favicon.png">
|
||
|
||
<span>技术文章摘抄</span>
|
||
|
||
</a>
|
||
|
||
</div>
|
||
|
||
<div class="book-menu uncollapsible">
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li><a href="/" class="current-tab">首页</a></li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li><a href="../">上一级</a></li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/00 由点及面,搭建你的 Java 并发知识网.md.html">00 由点及面,搭建你的 Java 并发知识网.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/01 为何说只有 1 种实现线程的方法?.md.html">01 为何说只有 1 种实现线程的方法?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/02 如何正确停止线程?为什么 volatile 标记位的停止方法是错误的?.md.html">02 如何正确停止线程?为什么 volatile 标记位的停止方法是错误的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/03 线程是如何在 6 种状态之间转换的?.md.html">03 线程是如何在 6 种状态之间转换的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/04 waitnotifynotifyAll 方法的使用注意事项?.md.html">04 waitnotifynotifyAll 方法的使用注意事项?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/05 有哪几种实现生产者消费者模式的方法?.md.html">05 有哪几种实现生产者消费者模式的方法?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/06 一共有哪 3 类线程安全问题?.md.html">06 一共有哪 3 类线程安全问题?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/07 哪些场景需要额外注意线程安全问题?.md.html">07 哪些场景需要额外注意线程安全问题?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/08 为什么多线程会带来性能问题?.md.html">08 为什么多线程会带来性能问题?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/09 使用线程池比手动创建线程好在哪里?.md.html">09 使用线程池比手动创建线程好在哪里?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/10 线程池的各个参数的含义?.md.html">10 线程池的各个参数的含义?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/11 线程池有哪 4 种拒绝策略?.md.html">11 线程池有哪 4 种拒绝策略?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/12 有哪 6 种常见的线程池?什么是 Java8 的 ForkJoinPool?.md.html">12 有哪 6 种常见的线程池?什么是 Java8 的 ForkJoinPool?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/13 线程池常用的阻塞队列有哪些?.md.html">13 线程池常用的阻塞队列有哪些?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/14 为什么不应该自动创建线程池?.md.html">14 为什么不应该自动创建线程池?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/15 合适的线程数量是多少?CPU 核心数和线程数的关系?.md.html">15 合适的线程数量是多少?CPU 核心数和线程数的关系?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/16 如何根据实际需要,定制自己的线程池?.md.html">16 如何根据实际需要,定制自己的线程池?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/17 如何正确关闭线程池?shutdown 和 shutdownNow 的区别?.md.html">17 如何正确关闭线程池?shutdown 和 shutdownNow 的区别?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/18 线程池实现“线程复用”的原理?.md.html">18 线程池实现“线程复用”的原理?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/19 你知道哪几种锁?分别有什么特点?.md.html">19 你知道哪几种锁?分别有什么特点?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/20 悲观锁和乐观锁的本质是什么?.md.html">20 悲观锁和乐观锁的本质是什么?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/21 如何看到 synchronized 背后的“monitor 锁”?.md.html">21 如何看到 synchronized 背后的“monitor 锁”?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/22 synchronized 和 Lock 孰优孰劣,如何选择?.md.html">22 synchronized 和 Lock 孰优孰劣,如何选择?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/23 Lock 有哪几个常用方法?分别有什么用?.md.html">23 Lock 有哪几个常用方法?分别有什么用?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/24 讲一讲公平锁和非公平锁,为什么要“非公平”?.md.html">24 讲一讲公平锁和非公平锁,为什么要“非公平”?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/25 读写锁 ReadWriteLock 获取锁有哪些规则?.md.html">25 读写锁 ReadWriteLock 获取锁有哪些规则?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/26 读锁应该插队吗?什么是读写锁的升降级?.md.html">26 读锁应该插队吗?什么是读写锁的升降级?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/27 什么是自旋锁?自旋的好处和后果是什么呢?.md.html">27 什么是自旋锁?自旋的好处和后果是什么呢?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/28 JVM 对锁进行了哪些优化?.md.html">28 JVM 对锁进行了哪些优化?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/29 HashMap 为什么是线程不安全的?.md.html">29 HashMap 为什么是线程不安全的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/30 ConcurrentHashMap 在 Java7 和 8 有何不同?.md.html">30 ConcurrentHashMap 在 Java7 和 8 有何不同?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/31 为什么 Map 桶中超过 8 个才转为红黑树?.md.html">31 为什么 Map 桶中超过 8 个才转为红黑树?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/32 同样是线程安全,ConcurrentHashMap 和 Hashtable 的区别.md.html">32 同样是线程安全,ConcurrentHashMap 和 Hashtable 的区别.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/33 CopyOnWriteArrayList 有什么特点?.md.html">33 CopyOnWriteArrayList 有什么特点?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/34 什么是阻塞队列?.md.html">34 什么是阻塞队列?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/35 阻塞队列包含哪些常用的方法?add、offer、put 等方法的区别?.md.html">35 阻塞队列包含哪些常用的方法?add、offer、put 等方法的区别?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/36 有哪几种常见的阻塞队列?.md.html">36 有哪几种常见的阻塞队列?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/37 阻塞和非阻塞队列的并发安全原理是什么?.md.html">37 阻塞和非阻塞队列的并发安全原理是什么?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/38 如何选择适合自己的阻塞队列?.md.html">38 如何选择适合自己的阻塞队列?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/39 原子类是如何利用 CAS 保证线程安全的?.md.html">39 原子类是如何利用 CAS 保证线程安全的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/40 AtomicInteger 在高并发下性能不好,如何解决?为什么?.md.html">40 AtomicInteger 在高并发下性能不好,如何解决?为什么?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/41 原子类和 volatile 有什么异同?.md.html">41 原子类和 volatile 有什么异同?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/42 AtomicInteger 和 synchronized 的异同点?.md.html">42 AtomicInteger 和 synchronized 的异同点?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/43 Java 8 中 Adder 和 Accumulator 有什么区别?.md.html">43 Java 8 中 Adder 和 Accumulator 有什么区别?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/44 ThreadLocal 适合用在哪些实际生产的场景中?.md.html">44 ThreadLocal 适合用在哪些实际生产的场景中?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/45 ThreadLocal 是用来解决共享资源的多线程访问的问题吗?.md.html">45 ThreadLocal 是用来解决共享资源的多线程访问的问题吗?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/46 多个 ThreadLocal 在 Thread 中的 threadlocals 里是怎么存储的?.md.html">46 多个 ThreadLocal 在 Thread 中的 threadlocals 里是怎么存储的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/47 内存泄漏——为何每次用完 ThreadLocal 都要调用 remove()?.md.html">47 内存泄漏——为何每次用完 ThreadLocal 都要调用 remove()?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/48 Callable 和 Runnable 的不同?.md.html">48 Callable 和 Runnable 的不同?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/49 Future 的主要功能是什么?.md.html">49 Future 的主要功能是什么?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/50 使用 Future 有哪些注意点?Future 产生新的线程了吗?.md.html">50 使用 Future 有哪些注意点?Future 产生新的线程了吗?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/51 如何利用 CompletableFuture 实现“旅游平台”问题?.md.html">51 如何利用 CompletableFuture 实现“旅游平台”问题?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/52 信号量能被 FixedThreadPool 替代吗?.md.html">52 信号量能被 FixedThreadPool 替代吗?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/53 CountDownLatch 是如何安排线程执行顺序的?.md.html">53 CountDownLatch 是如何安排线程执行顺序的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/54 CyclicBarrier 和 CountdownLatch 有什么异同?.md.html">54 CyclicBarrier 和 CountdownLatch 有什么异同?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/55 Condition、object.wait() 和 notify() 的关系?.md.html">55 Condition、object.wait() 和 notify() 的关系?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/56 讲一讲什么是 Java 内存模型?.md.html">56 讲一讲什么是 Java 内存模型?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/57 什么是指令重排序?为什么要重排序?.md.html">57 什么是指令重排序?为什么要重排序?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/58 Java 中的原子操作有哪些注意事项?.md.html">58 Java 中的原子操作有哪些注意事项?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/59 什么是“内存可见性”问题?.md.html">59 什么是“内存可见性”问题?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/60 主内存和工作内存的关系?.md.html">60 主内存和工作内存的关系?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/61 什么是 happens-before 规则?.md.html">61 什么是 happens-before 规则?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/62 volatile 的作用是什么?与 synchronized 有什么异同?.md.html">62 volatile 的作用是什么?与 synchronized 有什么异同?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/63 单例模式的双重检查锁模式为什么必须加 volatile?.md.html">63 单例模式的双重检查锁模式为什么必须加 volatile?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/64 你知道什么是 CAS 吗?.md.html">64 你知道什么是 CAS 吗?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/65 CAS 和乐观锁的关系,什么时候会用到 CAS?.md.html">65 CAS 和乐观锁的关系,什么时候会用到 CAS?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/66 CAS 有什么缺点?.md.html">66 CAS 有什么缺点?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/67 如何写一个必然死锁的例子?.md.html">67 如何写一个必然死锁的例子?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/68 发生死锁必须满足哪 4 个条件?.md.html">68 发生死锁必须满足哪 4 个条件?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/69 如何用命令行和代码定位死锁?.md.html">69 如何用命令行和代码定位死锁?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/70 有哪些解决死锁问题的策略?.md.html">70 有哪些解决死锁问题的策略?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/71 讲一讲经典的哲学家就餐问题.md.html">71 讲一讲经典的哲学家就餐问题.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
<a class="current-tab" href="/专栏/Java 并发编程 78 讲-完/72 final 的三种用法是什么?.md.html">72 final 的三种用法是什么?.md.html</a>
|
||
|
||
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/73 为什么加了 final 却依然无法拥有“不变性”?.md.html">73 为什么加了 final 却依然无法拥有“不变性”?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/74 为什么 String 被设计为是不可变的?.md.html">74 为什么 String 被设计为是不可变的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/75 为什么需要 AQS?AQS 的作用和重要性是什么?.md.html">75 为什么需要 AQS?AQS 的作用和重要性是什么?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/76 AQS 的内部原理是什么样的?.md.html">76 AQS 的内部原理是什么样的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/77 AQS 在 CountDownLatch 等类中的应用原理是什么?.md.html">77 AQS 在 CountDownLatch 等类中的应用原理是什么?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/78 一份独家的 Java 并发工具图谱.md.html">78 一份独家的 Java 并发工具图谱.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
|
||
|
||
<div class="sidebar-toggle-inner"></div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<script>
|
||
|
||
function add_inner() {
|
||
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
|
||
inner.classList.add('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
function remove_inner() {
|
||
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
|
||
inner.classList.remove('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
function sidebar_toggle() {
|
||
|
||
let sidebar_toggle = document.querySelector('.sidebar-toggle')
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let content = document.querySelector('.off-canvas-content')
|
||
|
||
if (sidebar_toggle.classList.contains('extend')) { // show
|
||
|
||
sidebar_toggle.classList.remove('extend')
|
||
|
||
sidebar.classList.remove('hide')
|
||
|
||
content.classList.remove('extend')
|
||
|
||
} else { // hide
|
||
|
||
sidebar_toggle.classList.add('extend')
|
||
|
||
sidebar.classList.add('hide')
|
||
|
||
content.classList.add('extend')
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
function open_sidebar() {
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
|
||
sidebar.classList.add('show')
|
||
|
||
overlay.classList.add('show')
|
||
|
||
}
|
||
|
||
function hide_canvas() {
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
|
||
sidebar.classList.remove('show')
|
||
|
||
overlay.classList.remove('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
</script>
|
||
|
||
|
||
|
||
<div class="off-canvas-content">
|
||
|
||
<div class="columns">
|
||
|
||
<div class="column col-12 col-lg-12">
|
||
|
||
<div class="book-navbar">
|
||
|
||
<!-- For Responsive Layout -->
|
||
|
||
<header class="navbar">
|
||
|
||
<section class="navbar-section">
|
||
|
||
<a onclick="open_sidebar()">
|
||
|
||
<i class="icon icon-menu"></i>
|
||
|
||
</a>
|
||
|
||
</section>
|
||
|
||
</header>
|
||
|
||
</div>
|
||
|
||
<div class="book-content" style="max-width: 960px; margin: 0 auto;
|
||
|
||
overflow-x: auto;
|
||
|
||
overflow-y: hidden;">
|
||
|
||
<div class="book-post">
|
||
|
||
<p id="tip" align="center"></p>
|
||
|
||
<div><h1>72 final 的三种用法是什么?</h1>
|
||
|
||
<p>本课时我们主要讲解 final 的三种用法。</p>
|
||
|
||
<h3>final 的作用</h3>
|
||
|
||
<p>final 是 Java 中的一个关键字,简而言之,final 的作用意味着“<strong>这是无法改变的</strong>”。不过由于 final 关键字一共有三种用法,它可以用来修饰<strong>变量</strong>、<strong>方法</strong>或者<strong>类</strong>,而且在修饰不同的地方时,效果、含义和侧重点也会有所不同,所以我们需要把这三种情况分开介绍。</p>
|
||
|
||
<p>我们先来看一下 final 修饰变量的情况。</p>
|
||
|
||
<h3>final 修饰变量</h3>
|
||
|
||
<h4>作用</h4>
|
||
|
||
<p>关键字 final 修饰变量的作用是很明确的,那就是意味着这个变量<strong>一旦被赋值就不能被修改了</strong>,也就是说只能被赋值一次,直到天涯海角也不会“变心”。如果我们尝试对一个已经赋值过 final 的变量再次赋值,就会报编译错误。</p>
|
||
|
||
<p>我们来看下面这段代码示例:</p>
|
||
|
||
<pre><code class="language-java">/**
|
||
|
||
|
||
|
||
* 描述: final变量一旦被赋值就不能被修改
|
||
|
||
|
||
|
||
*/
|
||
|
||
|
||
|
||
public class FinalVarCantChange {
|
||
|
||
|
||
|
||
public final int finalVar = 0;
|
||
|
||
|
||
|
||
public static void main(String[] args) {
|
||
|
||
|
||
|
||
FinalVarCantChange finalVarCantChange = new FinalVarCantChange();
|
||
|
||
|
||
|
||
// finalVarCantChange.finalVar=9; //编译错误,不允许修改final的成员变量
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>在这个例子中,我们有一个 final 修饰的 int,这个变量叫作 finalVar,然后在 main 函数中,新建了这个类的实例,并且尝试去修改它的值,此时会报编译错误,所以这体现了 final 修饰变量的一个最主要的作用:一旦被赋值就不能被修改了。</p>
|
||
|
||
<h4>目的</h4>
|
||
|
||
<p>看完了它的作用之后,我们就来看一下使用 final 的目的,也就是为什么要对某个变量去加 final 关键字呢?主要有以下两点目的。</p>
|
||
|
||
<p>第一个目的是出于<strong>设计角度</strong>去考虑的,比如我们希望创建一个一旦被赋值就不能改变的量,那么就可以使用 final 关键字。比如声明常量的时候,通常都是带 final 的:</p>
|
||
|
||
<pre><code class="language-java">public static final int YEAR = 2021;
|
||
|
||
</code></pre>
|
||
|
||
<p>这个时候其实 YEAR 是固定写死的,所以我们为了防止它被修改,就给它加上了 final 关键字,这样可以让这个常量更加清晰,也更不容易出错。</p>
|
||
|
||
<p>第二个目的是从<strong>线程安全的角度</strong>去考虑的。<strong>不可变</strong>的对象天生就是线程安全的,所以不需要我们额外进行同步等处理,这些开销是没有的。如果 final 修饰的是<strong>基本数据类型</strong>,那么它自然就具备了不可变这个性质,所以自动保证了线程安全,这样的话,我们未来去使用它也就非常放心了。</p>
|
||
|
||
<p>这就是我们使用 final 去修饰变量的两个目的。</p>
|
||
|
||
<h4>赋值时机</h4>
|
||
|
||
<p>下面我们就来看一下被 final 修饰的变量的赋值时机,变量可以分为以下三种:</p>
|
||
|
||
<ul>
|
||
|
||
<li>成员变量,类中的非 static 修饰的属性;</li>
|
||
|
||
<li>静态变量,类中的被 static 修饰的属性;</li>
|
||
|
||
<li>局部变量,方法中的变量。</li>
|
||
|
||
</ul>
|
||
|
||
<p>这三种不同情况的变量,被 final 修饰后,其赋值时机也各不相同,我们逐个来看一下。</p>
|
||
|
||
<p><strong>(1)成员变量</strong></p>
|
||
|
||
<p>成员变量指的是一个类中的非 static 属性,对于这种成员变量而言,被 final 修饰后,它有三种赋值时机(或者叫作赋值途径)。</p>
|
||
|
||
<ul>
|
||
|
||
<li>第一种是在声明变量的等号右边直接赋值,例如:</li>
|
||
|
||
</ul>
|
||
|
||
<pre><code class="language-java">public class FinalFieldAssignment1 {
|
||
|
||
|
||
|
||
private final int finalVar = 0;
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>在这个类中有 “private final int finalVar = 0” ,这就是在声明变量的时候就已经赋值了。</p>
|
||
|
||
<ul>
|
||
|
||
<li>第二种是在构造函数中赋值,例如:</li>
|
||
|
||
</ul>
|
||
|
||
<pre><code class="language-java">class FinalFieldAssignment2 {
|
||
|
||
|
||
|
||
private final int finalVar;
|
||
|
||
|
||
|
||
public FinalFieldAssignment2() {
|
||
|
||
|
||
|
||
finalVar = 0;
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>在这个例子中,我们首先声明了变量,即 private final int finalVar,且没有把它赋值,然后在这个类的构造函数中对它进行赋值,这也是可以的。</p>
|
||
|
||
<ul>
|
||
|
||
<li>第三种就是在类的构造代码块中赋值(不常用),例如:</li>
|
||
|
||
</ul>
|
||
|
||
<pre><code class="language-java">class FinalFieldAssignment3 {
|
||
|
||
|
||
|
||
private final int finalVar;
|
||
|
||
|
||
|
||
{
|
||
|
||
|
||
|
||
finalVar = 0;
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>我们同样也声明了一个变量 private final int finalVar,且没有把它赋值,然后在下面的一个由大括号括起来的类的构造代码块中,对变量进行了赋值,这也是合理的赋值时机。</p>
|
||
|
||
<p>需要注意的是,这里讲了三种赋值时机,我们<strong>必须从中挑一种来完成对 final 变量的赋值</strong>。如果不是 final 的普通变量,当然可以不用在这三种情况下赋值,完全可以在其他的时机赋值;或者如果你不准备使用这个变量,那么自始至终不赋值甚至也是可以的。但是对于 final 修饰的成员变量而言,必须在三种情况中任选一种来进行赋值,而不能一种都不挑、完全不赋值,那是不行的,这是 final 语法所规定的。</p>
|
||
|
||
<h4>空白 final</h4>
|
||
|
||
<p>下面讲解一种概念:“<strong>空白 final</strong>”。如果我们声明了 final 变量之后,并没有立刻在等号右侧对它赋值,这种情况就被称为“<strong>空白 final</strong>”。这样做的好处在于增加了 final 变量的灵活性,比如可以在构造函数中根据不同的情况,对 final 变量进行不同的赋值,这样的话,被 final 修饰的变量就不会变得死板,同时又能保证在赋值后保持不变。我们用下面这个代码来说明:</p>
|
||
|
||
<pre><code class="language-java">/**
|
||
|
||
|
||
|
||
* 描述: 空白final提供了灵活性
|
||
|
||
|
||
|
||
*/
|
||
|
||
|
||
|
||
public class BlankFinal {
|
||
|
||
|
||
|
||
//空白final
|
||
|
||
|
||
|
||
private final int a;
|
||
|
||
|
||
|
||
//不传参则把a赋值为默认值0
|
||
|
||
|
||
|
||
public BlankFinal() {
|
||
|
||
|
||
|
||
this.a = 0;
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
//传参则把a赋值为传入的参数
|
||
|
||
|
||
|
||
public BlankFinal(int a) {
|
||
|
||
|
||
|
||
this.a = a;
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>在这个代码中,我们有一个 private final 的 int 变量叫作 a,该类有两个构造函数,第一个构造函数是把 a 赋值为 0,第二个构造函数是把 a 赋值为传进来的参数,所以你调用不同的构造函数,就会有不同的赋值情况。这样一来,利用这个规则,我们就可以根据业务去给 final 变量设计更灵活的赋值逻辑。所以利用空白 final 的一大好处,就是可以让这个 final 变量的值并不是说非常死板,不是绝对固定的,而是可以根据情况进行灵活的赋值,只不过一旦赋值后,就不能再更改了。</p>
|
||
|
||
<p><strong>(2)静态变量</strong></p>
|
||
|
||
<p>静态变量是类中的 static 属性,它被 final 修饰后,只有两种赋值时机。</p>
|
||
|
||
<p>第一种同样是在声明变量的等号右边直接赋值,例如:</p>
|
||
|
||
<pre><code class="language-java">/**
|
||
|
||
|
||
|
||
* 描述: 演示final的static类变量的赋值时机
|
||
|
||
|
||
|
||
*/
|
||
|
||
|
||
|
||
public class StaticFieldAssignment1 {
|
||
|
||
|
||
|
||
private static final int a = 0;
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>第二种赋值时机就是它可以在一个静态的 static 初始代码块中赋值,这种用法不是很多,例如:</p>
|
||
|
||
<pre><code class="language-java">class StaticFieldAssignment2 {
|
||
|
||
|
||
|
||
private static final int a;
|
||
|
||
|
||
|
||
static {
|
||
|
||
|
||
|
||
a = 0;
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>在这个类中有一个变量 private static final int a,然后有一个 static,接着是大括号,这是静态初始代码块的语法,在这里面我们对 a 进行了赋值,这种赋值时机也是允许的。以上就是静态 final 变量的两种赋值时机。</p>
|
||
|
||
<p>需要注意的是,我们不能用普通的非静态初始代码块来给静态的 final 变量赋值。同样有一点比较特殊的是,这个 <strong>static 的 final 变量不能在构造函数中进行赋值</strong>。</p>
|
||
|
||
<p><strong>(3)局部变量</strong></p>
|
||
|
||
<p>局部变量指的是方法中的变量,如果你把它修饰为了 final,它的含义依然是<strong>一旦赋值就不能改变</strong>。</p>
|
||
|
||
<p>但是它的赋值时机和前两种变量是不一样的,因为它是在方法中定义的,所以它没有构造函数,也同样不存在初始代码块,所以对应的这两种赋值时机就都不存在了。实际上,对于 final 的局部变量而言,它是不限定具体赋值时机的,只要求我们<strong>在使用之前必须对它进行赋值</strong>即可。</p>
|
||
|
||
<p>这个要求和方法中的非 final 变量的要求也是一样的,对于方法中的一个非 final 修饰的普通变量而言,它其实也是要求在使用这个变量之前对它赋值。我们来看下面这个代码的例子:</p>
|
||
|
||
<pre><code class="language-java">/**
|
||
|
||
|
||
|
||
* 描述: 本地变量的赋值时机:使用前赋值即可
|
||
|
||
|
||
|
||
*/
|
||
|
||
|
||
|
||
public class LocalVarAssignment1 {
|
||
|
||
|
||
|
||
public void foo() {
|
||
|
||
|
||
|
||
final int a = 0;//等号右边直接赋值
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
class LocalVarAssignment2 {
|
||
|
||
|
||
|
||
public void foo() {
|
||
|
||
|
||
|
||
final int a;//这是允许的,因为a没有被使用
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
class LocalVarAssignment3 {
|
||
|
||
|
||
|
||
public void foo() {
|
||
|
||
|
||
|
||
final int a;
|
||
|
||
|
||
|
||
a = 0;//使用前赋值
|
||
|
||
|
||
|
||
System.out.println(a);
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>首先我们来看下第一个类,即 LocalVarAssignment1,然后在 foo() 方法中有一个 final 修饰的 int a,最后这里直接在等号右边赋值。</p>
|
||
|
||
<p>下面看第二个类,由于我们后期没有使用到这个 final 修饰的局部变量 a,所以这里实际上自始至终都没有对 a 进行赋值,即便它是 final 的,也可以对它不赋值,这种行为是语法所允许的。</p>
|
||
|
||
<p>第三种情况就是先创造出一个 final int a,并且不在等号右边对它进行赋值,然后在使用之前对 a 进行赋值,最后再使用它,这也是允许的。</p>
|
||
|
||
<p>总结一下,对于这种局部变量的 final 变量而言,它的赋值时机就是<strong>要求在使用之前进行赋值</strong>,否则使用一个未赋值的变量,自然会报错。</p>
|
||
|
||
<h4>特殊用法:final 修饰参数</h4>
|
||
|
||
<p>关键字 final 还可以用于修饰方法中的参数。在方法的参数列表中是可以把参数声明为 final 的,这意味着<strong>我们没有办法在方法内部对这个参数进行修改</strong>。例如:</p>
|
||
|
||
<pre><code class="language-java">/**
|
||
|
||
|
||
|
||
* 描述: final参数
|
||
|
||
|
||
|
||
*/
|
||
|
||
|
||
|
||
public class FinalPara {
|
||
|
||
|
||
|
||
public void withFinal(final int a) {
|
||
|
||
|
||
|
||
System.out.println(a);//可以读取final参数的值
|
||
|
||
|
||
|
||
// a = 9; //编译错误,不允许修改final参数的值
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>在这个代码中有一个 withFinal 方法,而且这个方法的入参 a 是被 final 修饰的。接下来,我们首先把入参的 a 打印出来,这是允许的,意味着我们可以读取到它的值;但是接下来我们假设想在方法中对这个 a 进行修改,比如改成 a = 9,这就会报编译错误,因此不允许修改 final 参数的值。</p>
|
||
|
||
<p>以上我们就把 final 修饰变量的情况都讲完了,其核心可以用一句话总结:<strong>一旦被赋值就不能被修改了</strong>。</p>
|
||
|
||
<h3>final 修饰方法</h3>
|
||
|
||
<p>下面来看一看 final 修饰方法的情况。选择用 final 修饰方法的原因之一是为了<strong>提高效率</strong>,因为在早期的 Java 版本中,会把 final 方法转为内嵌调用,可以消除方法调用的开销,以提高程序的运行效率。不过在后期的 Java 版本中,JVM 会对此自动进行优化,所以不需要我们程序员去使用 final 修饰方法来进行这些优化了,即便使用也不会带来性能上的提升。</p>
|
||
|
||
<p>目前我们使用 final 去修饰方法的唯一原因,就是想把这个方法锁定,意味着任何继承类都不能修改这个方法的含义,也就是说,被 final 修饰的方法<strong>不可以被重写</strong>,不能被 override。我们来举一个代码的例子:</p>
|
||
|
||
<pre><code class="language-java">/**
|
||
|
||
|
||
|
||
* 描述: final的方法不允许被重写
|
||
|
||
|
||
|
||
*/
|
||
|
||
|
||
|
||
public class FinalMethod {
|
||
|
||
|
||
|
||
public void drink() {
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
public final void eat() {
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
class SubClass extends FinalMethod {
|
||
|
||
|
||
|
||
@Override
|
||
|
||
|
||
|
||
public void drink() {
|
||
|
||
|
||
|
||
//非final方法允许被重写
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
// public void eat() {}//编译错误,不允许重写final方法
|
||
|
||
|
||
|
||
// public final SubClass() {} //编译错误,构造方法不允许被final修饰
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>在这个代码中一共有两个类,第一个是 FinalMethod,它里面有一个 drink 方法和 eat 方法,其中 eat 方法是被 final 修饰的;第二个类 SubClass 继承了前面的 FinalMethod 类。</p>
|
||
|
||
<p>然后我们去尝试对 drink 方法进行 Override,这当然是可以的,因为它是非 final 方法;接着尝试对 eat 方法进行 Override,你会发现,在下面的子类中去重写这个 eat 方法是不行的,会报编译错误,因为不允许重写 final 方法。</p>
|
||
|
||
<p>同时这里还有一个注意点,在下方我们又写了一个 public final SubClass () {},这是一个构造函数,这里也是编译不通过的,因为<strong>构造方法不允许被 final 修饰</strong>。</p>
|
||
|
||
<p><strong>特例:final 的 private方法</strong></p>
|
||
|
||
<p>这里有一个特例,那就是用 final 去修饰 private 方法。我们先来看看下面这个看起来可能不太符合规律的代码例子:</p>
|
||
|
||
<pre><code class="language-java">/**
|
||
|
||
|
||
|
||
* 描述: private方法隐式指定为final
|
||
|
||
|
||
|
||
*/
|
||
|
||
|
||
|
||
public class PrivateFinalMethod {
|
||
|
||
|
||
|
||
private final void privateEat() {
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
class SubClass2 extends PrivateFinalMethod {
|
||
|
||
|
||
|
||
private final void privateEat() {//编译通过,但这并不是真正的重写
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>在这个代码例子中,首先有个 PrivateFinalMethod 类,它有个 final 修饰的方法,但是注意这个方法是 private 的,接下来,下面的 SubClass2 extends 第一个 PrivateFinalMethod 类,也就是说继承了第一个类;然后子类中又写了一个 private final void privateEat() 方法,而且这个时候编译是通过的,也就是说,子类有一个方法名字叫 privateEat,而且是 final 修饰的。同样的,这个方法一模一样的出现在了父类中,那是不是说这个子类 SubClass2 成功的重写了父类的 privateEat 方法呢?是不是意味着我们之前讲的“被 final 修饰的方法,不可被重写”,这个结论是有问题的呢?</p>
|
||
|
||
<p>其实我们之前讲的结论依然是对的,但是类中的所有 private 方法都是隐式的指定为自动被 final 修饰的,我们额外的给它加上 final 关键字并不能起到任何效果。由于我们这个方法是 private 类型的,所以对于子类而言,根本就获取不到父类的这个方法,就更别说重写了。在上面这个代码例子中,其实<strong>子类并没有真正意义上的去重写父类的 privateEat 方法</strong>,子类和父类的这两个 privateEat 方法彼此之间是独立的,只是方法名碰巧一样而已。</p>
|
||
|
||
<p>为了证明这一点,我们尝试在子类的 privateEat 方法上加个 Override 注解,这个时候就会提示“Method does not override method from its superclass”,意思是“该方法没有重写父类的方法”,就证明了<strong>这不是一次真正的重写</strong>。</p>
|
||
|
||
<p>以上就把 final 修饰方法的情况讲解完了。</p>
|
||
|
||
<h3>final 修饰类</h3>
|
||
|
||
<p>下面我们再来看下 final 修饰类的情况,final 修饰类的含义很明确,就是这个类“<strong>不可被继承</strong>”。我们举个代码例子:</p>
|
||
|
||
<pre><code class="language-java">/**
|
||
|
||
|
||
|
||
* 描述: 测试final class的效果
|
||
|
||
|
||
|
||
*/
|
||
|
||
|
||
|
||
public final class FinalClassDemo {
|
||
|
||
|
||
|
||
//code
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
//class A extends FinalClassDemo {}//编译错误,无法继承final的类
|
||
|
||
</code></pre>
|
||
|
||
<p>有一个 final 修饰的类叫作 FinalClassDemo,然后尝试写 class A extends FinalClassDemo,结果会报编译错误,因为语法规定无法继承 final 类,那么我们给类加上 final 的目的是什么呢?如果我们这样设计,就代表不但我们自己不会继承这个类,也不允许其他人来继承,它就不可能有子类的出现,这在一定程度上可以<strong>保证线程安全</strong>。</p>
|
||
|
||
<p>比如非常经典的 String 类就是被 final 修饰的,所以我们自始至终也没有看到过哪个类是继承自 String 类的,这对于保证 String 的<strong>不可变性</strong>是很重要的,这一点我们会在第 74 讲展开讲解。</p>
|
||
|
||
<p>但这里有个注意点,假设我们给某个类加上了 final 关键字,这并不代表里面的成员变量自动被加上 final。事实上,这两者之间不存在相互影响的关系,也就是说,类是 final 的,不代表里面的属性就会自动加上 final。</p>
|
||
|
||
<p>不过我们也记得,final 修饰方法的含义就是这个方法不允许被重写,而现在如果给这个类都加了 final,那这个类连子类都不会有,就更不可能发生重写方法的情况。所以,其实在 final 的类里面,所有的方法,不论是 public、private 还是其他权限修饰符修饰的,都会<strong>自动的、隐式的被指定为是 final 修饰的</strong>。</p>
|
||
|
||
<h3>如果必须使用 final 方法或类,请说明原因</h3>
|
||
|
||
<p>这里有一个注意点,那就是如果我们真的要使用 final 类或者方法的话,需要注明原因。为什么呢?因为未来代码的维护者,他可能不是很理解为什么我们在这里使用了 final,因为使用后,对他来说是有影响的,比如用 final 修饰方法,那他就不能去重写了,或者说我们用 final 修饰了类,那他就不能去继承了。</p>
|
||
|
||
<p>所以为了防止后续维护者有困惑,我们其实是<strong>有必要或者说有义务说明原因</strong>,这样也不至于发生后续维护上的一些问题。</p>
|
||
|
||
<p>在很多情况下,我们并需要不急着把这个类或者方法声明为 final,可以到开发的中后期再去决定这件事情,这样的话,我们就能更清楚的明白各个类之间的交互方式,或者是各个方法之间的关系。所以你可能会发现根本就不需要去使用 final 来修饰,或者不需要把范围扩得太大,我们可以重构代码,把 final 应用在更小范围的类或方法上,这样造成更小的影响。</p>
|
||
|
||
<h3>总结</h3>
|
||
|
||
<p>本课时我们主要讲解了 final 的作用,它用在变量、方法或者类上时,其含义是截然不同的,所以我们就逐个对这 3 种情况进行了讲解:修饰变量意味着一旦被赋值就不能被修改;修饰方法意味着不能被重写;修饰类意味着不能被继承。</p>
|
||
|
||
<p>在讲解 final 修饰变量的时候,我们也分别对成员变量、静态变量和局部变量这三种不同的情况进行了展开分析,可以看到,它们的赋值时机也是各有不同的;如果我们利用空白 final,可以让变量变的更加灵活。还有一种特例那就是 final 去修饰参数,代表着不允许去改变这个参数的内容。</p>
|
||
|
||
<p>最后介绍了如果我们对方法或者类去使用 final 的话,最好能注明原因,描述清楚我们的设计思想。</p>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div>
|
||
|
||
<div style="float: left">
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/71 讲一讲经典的哲学家就餐问题.md.html">上一页</a>
|
||
|
||
</div>
|
||
|
||
<div style="float: right">
|
||
|
||
<a href="/专栏/Java 并发编程 78 讲-完/73 为什么加了 final 却依然无法拥有“不变性”?.md.html">下一页</a>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
|
||
|
||
</div>
|
||
|
||
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"7099710c8e603d60","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
|
||
|
||
</body>
|
||
|
||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||
|
||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
|
||
|
||
<script>
|
||
|
||
window.dataLayer = window.dataLayer || [];
|
||
|
||
|
||
|
||
function gtag() {
|
||
|
||
dataLayer.push(arguments);
|
||
|
||
}
|
||
|
||
|
||
|
||
gtag('js', new Date());
|
||
|
||
gtag('config', 'G-NPSEEVD756');
|
||
|
||
var path = window.location.pathname
|
||
|
||
var cookie = getCookie("lastPath");
|
||
|
||
console.log(path)
|
||
|
||
if (path.replace("/", "") === "") {
|
||
|
||
if (cookie.replace("/", "") !== "") {
|
||
|
||
console.log(cookie)
|
||
|
||
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
|
||
|
||
}
|
||
|
||
} else {
|
||
|
||
setCookie("lastPath", path)
|
||
|
||
}
|
||
|
||
|
||
|
||
function setCookie(cname, cvalue) {
|
||
|
||
var d = new Date();
|
||
|
||
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
|
||
|
||
var expires = "expires=" + d.toGMTString();
|
||
|
||
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
|
||
|
||
}
|
||
|
||
|
||
|
||
function getCookie(cname) {
|
||
|
||
var name = cname + "=";
|
||
|
||
var ca = document.cookie.split(';');
|
||
|
||
for (var i = 0; i < ca.length; i++) {
|
||
|
||
var c = ca[i].trim();
|
||
|
||
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
|
||
|
||
}
|
||
|
||
return "";
|
||
|
||
}
|
||
|
||
|
||
|
||
</script>
|
||
|
||
|
||
|
||
</html>
|
||
|