Week 3
week 3
完成底层篇,20%的进阶篇
底层知识
JVM
内存结构
-
运行时区域
- 程序计数器
线程独享
- 虚拟机栈(线程栈)
线程独享
- 本地方法栈:线程独享
- 堆(heap)
- 方法区(内含常量池)
在 JDK 8 之前的版本中,方法区是用于存储类信息、常量池、静态变量等的区域。然而,从 JDK 8 开始,JVM 中的方法区已经被移除了。在 JDK 8 中,取而代之的是元空间(Metaspace),它是 JVM 中的一个内存区域,用于存储类元数据、常量池、静态变量等。
-
堆外内存
堆外内存指的是Java进程可以使用的,不属于Java堆的内存。在Java中,堆内内存是由JVM自动管理的,而堆外内存则需要开发者手动管理。
堆外内存通常用于处理大量数据,如文件上传、网络数据传输、大数据处理等场景。使用堆外内存可以避免OOM等问题,因为堆外内存不受JVM堆大小的限制。
在Java中,可以使用java.nio包中的ByteBuffer来分配堆外内存。ByteBuffer提供了allocateDirect()方法,可以分配直接字节缓冲区,即堆外内存。使用完堆外内存后,需要手动调用ByteBuffer的clear()方法来释放内存。
需要注意的是,堆外内存的使用需要谨慎,如果使用不当会导致内存泄漏等问题。因此,在使用堆外内存时,需要仔细规划和管理内存。
import java.nio.ByteBuffer;
public class HeapMemoryExample {
public static void main(String[] args) {
// 分配1MB的堆外内存
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);
// 将数据写入堆外内存
buffer.put((byte) 123);
buffer.put((byte) 45);
buffer.put((byte) 67);
// 切换到读模式
buffer.flip();
// 从堆外内存中读取数据
byte b1 = buffer.get();
byte b2 = buffer.get();
byte b3 = buffer.get();
System.out.println("Read bytes: " + b1 + ", " + b2 + ", " + b3);
// 释放堆外内存
buffer.clear();
}
}
- TLAB(线程本地分配缓冲区)
TLAB是JVM为每个新创建的线程分配的一块私有内存区域。TLAB的主要目的是加速对象的分配过程,避免多线程环境下对同一内存区域的竞争,提高对象分配的效率。
当一个线程需要创建新的对象时,JVM首先会尝试在TLAB中分配内存。如果TLAB中有足够的空间,那么对象就会在TLAB中创建。如果TLAB的空间不足,JVM会在堆内存中分配一块新的内存区域给该线程,并更新TLAB的大小。
在大多数情况下,TLAB可以提高对象的分配速度,因为每个线程都有自己的私有内存区域,减少了线程间的竞争。但是,如果一个线程创建了大量的短生命周期的对象,TLAB可能会变得碎片化,这可能会导致垃圾收集器(GC)的运行效率降低。为了解决这个问题,JVM提供了-XX:TLABWasteTargetPercent参数(整数0-100),用于设置TLAB的浪费目标百分比(即超过这个阈值后JVM会调整TLAB)。当TLAB的空间浪费超过这个百分比时,JVM会尝试调整TLAB的大小。
- java中的对象一定在堆上分配吗
在Java中,对象通常是在堆(Heap)上分配的。堆是Java内存管理的一部分,用于存储动态分配的对象和数组。堆空间是由Java垃圾回收器(Garbage Collector)自动管理的,当对象不再被引用时,垃圾回收器会自动回收这些对象占用的内存。
然而,有一些特殊情况下,对象可能不会分配在堆上。例如,逃逸分析(Escape Analysis)优化可能导致对象在栈(Stack)上分配。这是一种JIT编译器(Just-In-Time Compiler)的优化技术,它可以在运行时分析代码,以确定某些对象是否可以被优化为栈上分配。如果JIT编译器确定一个对象不会逃逸出方法(即不会被外部引用),它可能会选择在栈上分配该对象,而不是在堆上。
此外,还有一些Java语言特性,如匿名内部类和lambda表达式,也可能导致对象在堆上分配。这些特性创建的对象通常具有较短的生命周期,因此它们会被分配在堆上。
总之,在大多数情况下,Java中的对象是在堆上分配的。但是,也有一些特殊情况下,对象可能会在栈上分配,这取决于JIT编译器的逃逸分析结果。
垃圾回收
GC算法
标记清除、引用计数、复制、标记压缩、分代回收、增量式回收
增量式回收
增量式回收的基本思想是避免一次性将所有垃圾进行处理,而是将垃圾收集线程和应用程序线程交替执行。在每次垃圾收集时,垃圾收集线程只处理一小片区域的内存空间,然后切换到应用程序线程;接着再切换回垃圾收集线程,依次反复,直到垃圾收集完成。
这种分阶段完成的垃圾回收方式,可以逐步完成垃圾回收,降低垃圾回收的总体成本,同时减少对应用程序的影响,提高系统的吞吐量和稳定性。
但是需要注意的是,由于线程切换和上下文转换的消耗,增量式回收可能会导致系统资源的额外开销,进而影响系统性能。因此,在实际应用中需要根据具体的系统和需求来权衡和选择适合的垃圾回收策略。
在 JVM 中配置增量式回收需要了解你的 JVM 垃圾收集器是否支持这种回收策略。目前,像 G1 这样的垃圾收集器支持增量式回收。
对于 G1 垃圾收集器,你可以通过以下 JVM 参数来配置:
XX:G1PeriodicGCInterval
:设置周期性垃圾回收的时间间隔。默认情况下,此值通常设置为5秒,即每隔5秒钟,G1垃圾收集器会检查一次Java堆内存的使用情况,并根据需要进行垃圾回收。但是,这个间隔时间可以根据具体的应用程序和垃圾收集策略进行调整。XX:G1PeriodicGCSystemLoadThreshold
:设置周期性垃圾回收的系统负载阈值。默认情况下,此值通常设置为5,即当JVM主机系统的平均一分钟系统负载值超过5时,将不会触发垃圾回收。但是,这个阈值可以根据具体的应用程序和系统环境进行调整。
GC参数
JVM的垃圾收集(GC)是自动进行的,用于回收不再使用的对象以释放内存空间。在JVM中,GC参数用于配置GC的方式和行为。以下是一些常见的GC参数:
XX:+UseG1GC
:启用G1垃圾收集器。XX:G1NewSizePercent
:设置新生代大小占整个堆内存的百分比,默认为3。XX:G1MaxNewSizePercent
:设置新生代最大大小占整个堆内存的百分比,默认为6。XX:G1MixedGCCountTarget
:设置混合回收的次数,默认为8。XX:G1MixedGCLiveThresholdPercent
:设置混合回收的阈值,默认为92。XX:G1RSetUpdatingPauseTimePercent
:设置RSet更新时的暂停时间百分比,默认为10。XX:G1SummarizeRSetStatsPeriod
:设置RSet统计摘要的时间间隔,默认为20。XX:G1PeriodicGCInterval
:设置周期性垃圾回收的时间间隔,默认为5。XX:G1PeriodicGCSystemLoadThreshold
:设置周期性垃圾回收的系统负载阈值,默认为5。
垃圾回收器
- CMS(过时)
CMS(Concurrent Mark Sweep)是Java HotSpot虚拟机中的一种垃圾收集器,被设计用于服务端应用。它的主要优点是并发性,即在进行垃圾回收的同时,应用程序也可以继续运行。
CMS垃圾收集器的工作过程分为四个阶段:
- 初始标记(Initial Mark):这个阶段会标记所有从根对象直接可达的对象。这个阶段是阻塞的,意味着应用程序在初始标记阶段需要暂停。
- 并发标记(Concurrent Marking):在这个阶段,垃圾收集器会并发地遍历整个堆内存,标记所有可达的对象。这个阶段与应用程序并发运行,因此不会阻塞应用程序。
- 重新标记(Remark):这个阶段是为了修正并发标记阶段结束后可能存在的错误。在并发标记阶段,由于应用程序和垃圾收集器并发运行,可能会导致一些对象的状态错误。重新标记阶段会再次从根对象开始遍历,修复这些错误。这个阶段也是阻塞的,但时间较短。
- 清理(Sweep):这个阶段会清理所有未被标记的对象,并更新堆内存的空闲列表。这个阶段也是可以与应用程序并发运行的。
尽管CMS提供了并发性,但由于其需要维护额外的数据结构和算法,因此比不上G1和ZGC的效率。而且,由于CMS的并发标记和重新标记阶段不能保证完全的并发性,因此在高并发的情况下,可能会导致垃圾收集暂停时间过长。
- G1(生产环境)
G1(Garbage-First)是Java HotSpot虚拟机中的一种垃圾收集器,被设计用于服务端应用。G1将堆内存划分为多个独立的子区域,并对这些子区域进行独立的管理和回收。
G1垃圾收集器的工作过程可以分为以下几个阶段:
- 并发标记阶段(Concurrent Marking):在这个阶段,垃圾收集器会并发地遍历整个堆内存,标记所有可达的对象。这个阶段与应用程序并发运行,因此不会阻塞应用程序。
- 混合回收阶段(Mixed GC):在这个阶段,G1会同时收集新生代和老年代的对象。它会优先回收垃圾较多的区域,以尽可能地减少应用程序的暂停时间。
- Full GC阶段:当并发标记阶段或混合回收阶段无法满足系统的性能目标时,G1会触发Full GC。Full GC会暂停应用程序,并回收整个堆内存中的所有对象。
G1垃圾收集器的优点包括:
- 可预测的暂停时间:由于G1对堆内存进行了精细的管理,可以优先回收垃圾较多的区域,从而尽可能地减少应用程序的暂停时间。
- 并发性:G1在大多数情况下可以与应用程序并发运行,从而提高了系统的吞吐量。
- 多核并行回收:G1支持多核并行回收,可以充分利用多核CPU的性能优势,提高垃圾回收的效率。
- ZGC(生产环境)
ZGC(Z Garbage Collector)是Java HotSpot虚拟机中的一种垃圾收集器,它被设计成一款具有低延迟、高吞吐量以及适用于大内存的垃圾收集器。
ZGC在内存布局,算法和回收策略上都有一定的创新。
ZGC的内存布局采用染色指针(Colored Pointer)技术,通过在指针上附加颜色信息,来区分对象是否在收集过程中,以及是否可达。
ZGC的算法采用了读屏障(Read Barrier)和染色指针(Colored Pointer)等技术,使得在进行垃圾回收时,不需要停止应用程序的运行,从而实现了低延迟的垃圾回收。
ZGC的回收策略采用延迟回收(Lazy Collection)和优先回收(Priority Collection)的方式,通过优化算法和内存布局,尽可能地减少应用程序的暂停时间。
需要注意的是,尽管ZGC提供了高性能和低延迟的垃圾回收,但由于其需要维护额外的数据结构和算法,因此比不上G1和CMS的效率。此外,ZGC需要较大的内存空间来存储元数据,因此在使用时需要考虑内存空间的需求。
- Epsilon(测试环境)
Epsilon垃圾收集器是Java虚拟机(JVM)中的无操作垃圾收集器。它的主要作用是控制内存分配,但并不执行任何垃圾回收工作。当Java堆内存耗尽时,JVM会直接关闭。
这种垃圾收集器的设计目的主要是提供一个完全消极的GC实现,分配有限的内存,并最大限度地降低消费内存占用量和内存吞吐时的延迟时间。它的优点是可以隔离代码变化,不影响其他GC,并且能最小限度地改变其他的JVM代码。
Epsilon垃圾收集器在特定的使用场景下,如性能测试,以及需要差异性分析的场合,非常适合使用。它可以用于过滤由垃圾收集线程调度、垃圾屏障消耗、不合适触发垃圾周期等操作诱发的延迟。
使用:java启动时配置 -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC
JVM参数及优化
- Xmx和-Xms:这两个参数分别用来指定Java堆的最大值和初始值。-Xmx默认值是物理内存的1/4(但不超过1GB),-Xms默认值是物理内存的1/64(但不超过1GB)。在开发过程中,通常会将-Xms和-Xmx两个参数配置成相同的值,以避免在Java垃圾回收机制清理堆区后不需要重新分隔计算堆区的大小而浪费资源。在具体调优过程中,可以根据实际需要调整这两个参数的大小,以获得更好的性能。
- Xmn:这个参数用来指定Java堆中新生代的大小。这个参数对性能影响较大,如果程序需要比较多的临时内存,可以适当增大这个参数的值,例如设置到512M。但需要注意的是,如果新生代过小,会导致频繁的垃圾回收,从而影响性能。
- Xss:JVM 中的 Xss 参数的默认值因 JVM 版本而异。在 JDK 5 和 6 中,默认值为 256k,而在 JDK 7 及更高版本中,默认值为 1M。
- XX:PermSize和-XX:MaxPermSize:这两个参数用来指定Java堆中Perm Generation的最小值和最大值。Perm Generation是用于存储类的元数据信息,如果程序有大规模的递归行为,可能需要增大Perm Generation的大小。但需要注意的是,如果设置过大,会导致其他部分内存不足,从而影响性能。
- XX:NewRatio:这个参数用来指定新生代和老年代的比值。在Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。
- XX:SurvivorRatio:这个参数用来设置两个Survivor区和eden的比值。
- XX:MaxTenuringThreshold:用于设置对象进入老年代的年龄阈值,默认15。当一个对象经过多次垃圾回收后仍然存活,就会被晋升到老年代中。
JVM性能监控与故障处理工具
jps
jps [options] [hostid]
其中,[options]为选项,[hostid]为主机ID。常用的选项有:
- q:仅输出VM标识符,不包括classname,jar name,arguments in main method
- m:输出main method的参数
- l:输出完全的包名,应用主类名,jar的完全路径名
- v:输出jvm参数
- V:输出通过flag文件传递到JVM中的参数(.hotspotrc文件或-XX:Flags=所指定的文件)
- Joption:传递参数到vm,例如:-J-Xms512m
jstack
jstack [options] <PID>
其中,[options]为选项,为Java进程的标识符。常用的选项有:
- l:除堆栈外,打印关于锁的附加信息(如果可用)
- m:打印本地C/C++堆栈跟踪(如果可用)
- F:当正常输出失败时,强制执行堆栈跟踪(默认情况下不执行此操作)
- h:以易读的方式显示时间戳(默认情况下使用UNIX时间)
- -pid=:指定要生成线程转储的进程ID
例如,要生成进程ID为12345的Java线程转储快照,可以使用以下命令:
jstack 12345 > thread_dump.txt
jmap
jmap命令是一个可以输出所有内存中对象的工具,甚至可以将VM 中的heap,以二进制输出成文本。打印出某个java进程(使用pid)内存内的,所有‘对象’的情况(如:产生那些对象,及其数量)。 jmap命令可以用来查看内存信息,实例个数以及占用内存大小,常用于线上问题排查,如CPU占用过高,内存占用过高等。
常用的选项有:
- histo:输出堆中对象的统计信息
- dump:生成Java虚拟机的堆转储快照dump文件
- finalizeinfo:查看finalize执行队列
- F:强制执行垃圾回收器
- options:JVM运行时参数
例如:
jmap -dump:format=b,file=test.hprof 12345
这个命令会生成一个名为 test.hprof 的堆转储快照文件。
jstat
PS Z:\IDEA Project\springboot-demo-master> jstat -gcutil 3836 1000 5
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
36.91 0.00 96.61 0.87 96.05 92.10 4 0.018 1 0.008 0.026
36.91 0.00 96.61 0.87 96.05 92.10 4 0.018 1 0.008 0.026
36.91 0.00 97.01 0.87 96.05 92.10 4 0.018 1 0.008 0.026
每1000毫秒(即1秒)查询一次进程ID为3836的Java进程的垃圾收集统计信息,共查询5次。
jconsole
jconsole是Java自带的一个监控和管理Java应用程序的工具,可以用来监控JVM的各种指标,包括内存使用情况,在控制台输入jconsole直接弹出。
jinfo
得到JVM的详细信息
javap
反编译,与javac相反
C:\Users\Administrator\Desktop>javap Hello.class
Compiled from "Hello.java"
public class Hello {
public Hello();
public static void main(java.lang.String[]);
}
btrace
btrace -o /tmp/btrace.log -n 10000 -t 500 java -jar myapp.jar
该命令会每隔500毫秒记录一次Java应用程序的状态信息,共记录10000次。可以通过查看/tmp/btrace.log文件来了解应用程序的性能瓶颈和异常情况。
类加载机制
classLoader
Java中的类加载机制是指将类的字节码文件加载到JVM中,并解析成对应的Class对象的过程。类加载器(ClassLoader)是Java类加载机制的核心,它负责在运行时动态地加载Java类。
Java中的类加载器分为以下几种:
- 启动类加载器(Bootstrap ClassLoader):它是最顶层的类加载器,用于加载Java核心库(如java.lang.*),是由JVM自动创建的。
- 扩展类加载器(Extension ClassLoader):它是加载Java扩展库(如javax.*)的类加载器,由各个Java应用程序的类路径中的jar包或目录中的classes目录中的.class文件所指定。
- 应用类加载器(Application ClassLoader):它是针对具体的Java应用程序的类加载器,由sun.misc.Launcher$AppClassLoader实现。该类加载器会优先使用已经加载的类,如果没有找到相应的类,则会从扩展类加载器和启动类加载器中查找。
- 自定义类加载器(Custom ClassLoader):用户可以继承ClassLoader类,并重写其findClass方法来实现自定义的类加载器。
当一个Java程序需要加载一个类时,首先由该程序的类加载器进行尝试,如果无法加载则由其父类加载器依次尝试,直到启动类加载器。如果仍然无法加载,则会抛出ClassNotFoundException异常。
在Java中,可以通过URLClassLoader、FileClassLoader等类来创建自定义的类加载器,以满足特定的需求。同时,也可以通过反射机制动态地调用ClassLoader的方法,以实现类的动态加载和卸载。
类加载过程
Java中的类加载过程包括以下几个步骤:
- 加载(Loading):通过类的全限定名查找对应的.class文件,并将其读入到JVM中。
- 验证(Verification):对读入的.class文件进行验证,确保其符合Java虚拟机规范。
- 准备(Preparation):为类的静态变量分配内存并设置默认值。
- 解析(Resolution):将符号引用转换为直接引用。
- 初始化(Initialization):执行静态初始化器
()方法,以及初始化静态变量。 - 使用(Using):使用该类创建对象、调用方法等。
- 卸载(Unloading):当该类不再被使用时,将其从JVM中卸载。
在Java中,类加载器负责管理类的加载和卸载过程。当一个类需要被加载时,首先由该类的类加载器进行尝试,如果无法加载则由其父类加载器依次尝试,直到启动类加载器。如果仍然无法加载,则会抛出ClassNotFoundException异常。
在自定义类加载器中,可以通过重写findClass方法来实现自定义的类加载逻辑。该方法需要先调用父类的loadClass方法来获取Class对象,然后再进行一些额外的操作,如读取资源文件、编译字节码等。同时,也可以通过反射机制动态地调用ClassLoader的方法,以实现类的动态加载和卸载。
线程安全
在Java中,类加载器是线程安全的。当一个类需要被加载时,首先由该类的类加载器进行尝试,如果无法加载则由其父类加载器依次尝试,直到启动类加载器。如果仍然无法加载,则会抛出ClassNotFoundException异常。
在自定义类加载器中,可以通过重写findClass方法来实现自定义的类加载逻辑。该方法需要先调用父类的loadClass方法来获取Class对象,然后再进行一些额外的操作,如读取资源文件、编译字节码等。同时,也可以通过反射机制动态地调用ClassLoader的方法,以实现类的动态加载和卸载。
由于类加载过程是由JVM内部的类加载器完成的,因此不存在多线程访问同一个类的问题。即使在多线程环境下,不同的线程可以同时加载不同的类,也不会出现线程安全问题。
但是,如果在多线程环境下对同一个类进行了修改操作(如添加、修改或删除类的成员变量),则可能会出现线程安全问题。在这种情况下,可以使用synchronized关键字或者Lock接口来保证线程安全。例如,在修改类的成员变量时,可以使用synchronized关键字来保证在同一时刻只有一个线程可以对该变量进行修改。
双亲委派机制
双亲委派机制是指当一个类加载器收到一个类加载请求时,该类加载器首先会把请求委派给父类加载器。每个类加载器都是如此,只有在父类加载器在自己的搜索范围内找不到指定类时,子类加载器才会尝试自己去加载。这样保证了Java核心库的类型安全,避免类的重复加载。同时,双亲委派机制也保证了Java程序的安全性,防止核心API被随意篡改 。
如果你想要破坏它,你可以通过以下方式实现:
- 通过重写ClassLoader的loadClass方法,直接使用自己的类加载器来加载类,从而绕过双亲委派机制。
- 通过动态代理的方式,创建一个自己的类加载器,然后通过该类加载器来加载类,从而绕过双亲委派机制。
这些都是一些不安全的操作,不建议在实际项目中使用。
模块化
类的加载过程包括以下步骤:
- 装载:查找并加载类的二进制数据。
- 链接:验证、准备和解析步骤,其中解析步骤是可以选择的。
- 校验:检查载入Class文件数据的正确性。
- 准备:给类的静态变量分配存储空间。
- 解析:将符号引用转成直接引用。
- 初始化:对类的静态变量、静态代码块执行初始化工作。
模块化是指将一个大型程序拆分成多个小模块,每个模块都有自己的类加载器。在模块化中,每个模块都有自己的classpath,这样可以避免了类名冲突的问题。在模块化中,每个模块都有自己的module-info.java文件,用于描述模块的信息和依赖关系。当一个模块被加载时,它会先读取module-info.java文件中的信息,然后根据这些信息来加载自己所需的类。
编译与反编译
javac、javap
jad
Java反编译工具有很多种,其中比较常用的是jad命令。jad是一款老牌的、经典的、使用起来简单的Java反编译工具。它可以将.class文件反编译成对应的.java文件 。
你可以从官网下载jad工具并解压,然后将jad.exe执行目录配置到系统path路径中即可使用(如Java配置环境变量) 。
CRF
CFR是一款Java反编译工具,支持Java 7、Java 8的反编译,能解决jd-gui部分代码不能反编译的问题,尤其是匿名类、内部类的一些逻辑。你可以通过官网下载CFR并解压,然后将jad.exe执行目录配置到系统path路径中即可使用 。
进阶知识
Java底层
字节码
Java字节码是一种中间代码,它是Java源代码在编译后生成的二进制文件。Java字节码可以被Java虚拟机(JVM)执行,也可以被其他工具反编译成Java源代码。
Java字节码是平台无关的,因此可以在不同操作系统上运行。它包含了Java源代码的所有信息,包括变量、方法、控制流等。Java字节码可以通过Java编译器将Java源代码编译成字节码,也可以通过Java反编译器将字节码转换回Java源代码。
Java字节码的优点是可以在不同的平台上运行,而且可以动态生成,这使得Java程序具有很好的灵活性和扩展性。但是,由于字节码不是直接可读的文本,因此需要使用反编译器将其转换为源代码才能进行调试和修改。
class文件格式
Java class文件格式是一种二进制文件格式,用于存储Java程序的字节码。它包含了Java虚拟机(JVM)可以执行的指令、数据类型和结构信息。class文件可以分为三个部分:魔数、常量池和类的结构。
- 魔数(Magic Number):用于标识一个class文件,它是固定的4个字节,值为0xCAFEBABE。
- 常量池(Constant Pool):包含了类的各种字面量和符号引用,如类名、方法名、字段名等。常量池是有序的,按照顺序排列在class文件中。每个常量池项占用2字节或4字节,取决于其值的大小。
- 类的结构(Class Structure):包括类的版本信息、访问标志、父类索引、接口计数、字段和方法等信息。类的结构是有序的,按照代码执行的顺序排列在class文件中。
class文件可以通过Java编译器(javac)将Java源代码编译成字节码文件(.class文件),也可以通过Java虚拟机(JVM)加载和执行。
位运算
Java中的位运算包括以下几种:
- 按位与(&):对两个二进制数进行按位与操作,只有当两个对应位都为1时,结果才为1,否则为0。
- 按位或(|):对两个二进制数进行按位或操作,只要有一个对应位为1时,结果就为1,否则为0。
- 按位异或(^):对两个二进制数进行按位异或操作,如果两个对应位值相同,则结果为0,否则为1。
- 按位取反(~):对一个二进制数进行按位取反操作,将每个位上的0和1互换。
- 左移(<<):将一个二进制数向左移动指定的位数,右边空出的位用0填充。
- 右移(>>):将一个二进制数向右移动指定的位数,左边空出的位用符号位填充。
- 无符号右移(>>>):将一个二进制数向右移动指定的位数,左边空出的位用0填充。
- 左移运算符(<<):将一个整数向左移动指定的位数,右边空出的位用0填充。
- 右移运算符(>>):将一个整数向右移动指定的位数,左边空出的位用符号位填充。
- 无符号右移运算符(>>>):将一个整数向右移动指定的位数,左边空出的位用0填充。
设计模式
六大原则
- 单一职责原则:一个类应该只有一个引起它变化的原因。
- 开放封闭原则:一个软件实体应该对扩展开放,对修改关闭。
- 里氏替换原则:子类可以替换父类并且不影响程序的正确性。
- 依赖倒置原则:高层模块不应该依赖于低层模块,两者都应该依赖于抽象。
假设有一个汽车工厂,生产汽车需要很多零部件,比如发动机、轮胎、方向盘等等。这些零部件都是具体的实现,而汽车则是高层模块,是整个系统的设计目标。按照依赖倒置原则,我们不应该让汽车直接依赖于这些具体的零部件,而是应该把它们都抽象成通用的接口或者抽象类,然后通过依赖注入的方式来实现对它们的调用。这样设计出来的系统更加灵活、可扩展和易于维护。
- 接口隔离原则:客户端不应该依赖于它不需要使用的接口。
- 迪米特法则:一个对象应该对其他对象有尽可能少的了解。
创建型设计模式
- 单例模式
- 抽象工厂模式
抽象工厂模式是一种创建型设计模式,它提供了一种方式,可以将一组具有同一主题的单独工厂封装起来。在抽象工厂模式中,每个工厂都能生产一组相关的产品,例如创建按钮、文本框等界面组件。
首先,我们定义一个抽象产品接口:
public interface Button {
void click();
}
public interface TextBox {
void setText(String text);
String getText();
}
然后,我们定义两个具体的产品类:
public class WindowsButton implements Button {
@Override
public void click() {
System.out.println("Windows button clicked");
}
}
public class MacOSButton implements Button {
@Override
public void click() {
System.out.println("MacOS button clicked");
}
}
public class WindowsTextBox implements TextBox {
private String text;
@Override
public void setText(String text) {
this.text = text;
}
@Override
public String getText() {
return text;
}
}
public class MacOSTextBox implements TextBox {
private String text;
@Override
public void setText(String text) {
this.text = text;
}
@Override
public String getText() {
return text;
}
}
接下来,我们定义一个抽象工厂接口:
public interface GUIFactory {
Button createButton();
TextBox createTextBox();
}
然后,我们定义两个具体的工厂类:
public class WindowsFactory implements GUIFactory {
@Override
public Button createButton() {
return new WindowsButton();
}
@Override
public TextBox createTextBox() {
return new WindowsTextBox();
}
}
public class MacOSFactory implements GUIFactory {
@Override
public Button createButton() {
return new MacOSButton();
}
@Override
public TextBox createTextBox() {
return new MacOSTextBox();
}
}
最后,我们可以使用以下代码来测试我们的抽象工厂模式:
public class Client {
public static void main(String[] args) {
// 创建Windows工厂对象
GUIFactory factory1 = new WindowsFactory();
// 创建Windows按钮和文本框对象
Button button1 = factory1.createButton();
TextBox textBox1 = factory1.createTextBox();
button1.click(); // 输出 "Windows button clicked"
System.out.println(textBox1.getText()); // 输出 ""
// 创建MacOS工厂对象
GUIFactory factory2 = new MacOSFactory();
// 创建MacOS按钮和文本框对象
Button button2 = factory2.createButton();
TextBox textBox2 = factory2.createTextBox();
button2.click(); // 输出 "MacOS button clicked"
System.out.println(textBox2.getText()); // 输出 ""
}
}
- 建造者模式
建造者模式是一种创建型设计模式,它提供了一种方式,可以将一个复杂对象的构建与其表示分离开来。
首先,我们定义一个产品类:
public class Product {
private String partA;
private String partB;
private String partC;
// getter setter
public void show() {
System.out.println("Product: " + partA + ", " + partB + ", " + partC);
}
}
然后,我们定义一个抽象建造者类:
public abstract class Builder {
protected Product product = new Product();
public abstract void buildPartA();
public abstract void buildPartB();
public abstract void buildPartC();
public Product getResult() {
return product;
}
}
接下来,我们定义具体的建造者类:
public class ConcreteBuilder extends Builder {
public void buildPartA() {
product.setPartA("Part A");
}
public void buildPartB() {
product.setPartB("Part B");
}
public void buildPartC() {
product.setPartC("Part C");
}
}
最后,我们可以使用以下代码来测试我们的建造者模式:
public class BuilderPatternDemo {
public static void main(String[] args) {
Builder builder = new ConcreteBuilder();
builder.buildPartA();
builder.buildPartB();
builder.buildPartC();
Product product = builder.getResult();
product.show(); // 输出 "Product: Part A, Part B, Part C"
}
}
- 工厂模式
- 原型模式
原型模式是一种创建型设计模式,它提供了一种方式,可以在运行时动态地创建对象。
以下是使用Java实现原型模式的示例代码:
首先,我们定义一个产品接口:
public interface Product {
void use();
}
然后,我们定义一个具体产品类:
public class ConcreteProduct implements Product {
@Override
public void use() {
System.out.println("Using concrete product");
}
}
接下来,我们定义一个原型类:
public abstract class Prototype implements Cloneable {
protected Product product;
public abstract void clone();
public Product getProduct() {
return product;
}
}
然后,我们定义一个具体原型类:
public class ConcretePrototype extends Prototype {
public ConcretePrototype(Product product) {
this.product = product;
}
@Override
public void clone() {
try {
ConcretePrototype cloned = (ConcretePrototype) super.clone();
cloned.product = new ConcreteProduct(); // 创建新的具体产品对象
return cloned;
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // Can't happen
}
}
}
最后,我们可以使用以下代码来测试我们的原型模式:
public class PrototypePatternDemo {
public static void main(String[] args) {
Prototype prototype1 = new ConcretePrototype(new ConcreteProduct());
Product product1 = prototype1.getProduct();
product1.use(); // 输出 "Using concrete product"
Prototype prototype2 = prototype1.clone();
Product product2 = prototype2.getProduct();
product2.use(); // 输出 "Using concrete product"
}
}
结构型设计模式
- 适配器模式
适配器模式是一种结构型设计模式,它允许将一个类的接口转换成客户端所期望的另一个接口。
首先,我们定义一个目标接口:
public interface Target {
void request();
}
然后,我们定义一个需要适配的类:
public class Adaptee {
public void specificRequest() {
System.out.println("Adaptee's specific request");
}
}
接下来,我们定义一个适配器类:
public class Adapter implements Target {
private Adaptee adaptee;
public Adapter(Adaptee adaptee) {
this.adaptee = adaptee;
}
@Override
public void request() {
adaptee.specificRequest();
}
}
最后,我们可以使用以下代码来测试我们的适配器模式:
public class AdapterPatternDemo {
public static void main(String[] args) {
Adaptee adaptee = new Adaptee();
Target target = new Adapter(adaptee);
target.request(); // 输出 "Adaptee's specific request"
}
}
- 桥接模式
桥接模式是一种结构型设计模式,它允许将抽象部分与实现部分分离,使它们可以独立地变化。
首先,我们定义一个抽象类:
public abstract class Abstraction {
protected ConcreteImplementor concreteImplementor;
public void setConcreteImplementor(ConcreteImplementor concreteImplementor) {
this.concreteImplementor = concreteImplementor;
}
public abstract void operation();
}
然后,我们定义一个具体实现类:
public class ConcreteImplementor {
public void operation() {
System.out.println("ConcreteImplementor's operation");
}
}
接下来,我们定义一个扩展抽象类的类:
public class RefinedAbstraction extends Abstraction {
@Override
public void operation() {
concreteImplementor.operation();
}
}
最后,我们可以使用以下代码来测试我们的桥接模式:
public class BridgePatternDemo {
public static void main(String[] args) {
Abstraction abstraction = new RefinedAbstraction();
abstraction.setConcreteImplementor(new ConcreteImplementor());
abstraction.operation(); // 输出 "ConcreteImplementor's operation"
}
}
- 装饰模式
装饰模式是一种结构型设计模式,它允许将对象包装在一个装饰类中,从而可以在运行时动态地添加新的行为。
首先,我们定义一个抽象组件类:
public abstract class Component {
public void operation() {
System.out.println("Component's operation");
}
}
然后,我们定义一个具体组件类:
public class ConcreteComponent extends Component {
@Override
public void operation() {
System.out.println("ConcreteComponent's operation");
}
}
接下来,我们定义一个抽象装饰类:
public abstract class Decorator implements Component {
protected Component component;
public Decorator(Component component) {
this.component = component;
}
@Override
public void operation() {
component.operation();
}
}
然后,我们定义一个具体装饰类:
public class ConcreteDecoratorA extends Decorator {
public ConcreteDecoratorA(Component component) {
super(component);
}
@Override
public void operation() {
super.operation();
addedBehavior();
}
private void addedBehavior() {
System.out.println("ConcreteDecoratorA's behavior");
}
}
最后,我们可以使用以下代码来测试我们的装饰模式:
public class DecoratorPatternDemo {
public static void main(String[] args) {
Component component = new ConcreteComponent();
Component decoratorA = new ConcreteDecoratorA(component);
decoratorA.operation(); // 输出 "ConcreteComponent's operation" 和 "ConcreteDecoratorA's behavior"
}
}
- 组合模式
组合模式是一种结构型设计模式,它可以让你将对象组织成树形结构,并且可以对这些对象进行封装。
首先,我们定义一个抽象组件类Component:
public abstract class Component {
protected Component parent;
protected List<Component> children = new ArrayList<>();
public void add(Component component) {
if (component.parent != null) {
component.parent.remove(component);
}
component.parent = this;
children.add(component);
}
public void remove(Component component) {
children.remove(component);
component.parent = null;
}
public abstract void operation();
}
然后,我们定义两个具体的组件类ConcreteComponentA和ConcreteComponentB,它们都继承自Component类,并实现了operation()方法:
public class ConcreteComponentA extends Component {
@Override
public void operation() {
System.out.println("ConcreteComponentA operation");
}
}
public class ConcreteComponentB extends Component {
@Override
public void operation() {
System.out.println("ConcreteComponentB operation");
}
}
接下来,我们定义一个叶子组件类Leaf,它也继承自Component类,但是不需要实现operation()方法:
public class Leaf extends Component {
@Override
public void operation() {
// do nothing
}
}
最后,我们可以创建一个客户端程序来测试组合模式:
public class Client {
public static void main(String[] args) {
// 创建组件树
Component root = new ConcreteComponentA();
Component branch1 = new ConcreteComponentB();
Component branch2 = new ConcreteComponentB();
root.add(branch1);
root.add(branch2);
// 执行操作
root.operation();
branch1.operation();
branch2.operation();
// 移除组件
root.remove(branch1);
root.operation();
branch1.operation();
}
}
运行客户端程序,输出结果如下:
ConcreteComponentA operation
ConcreteComponentB operation
ConcreteComponentB operation
ConcreteComponentA operation
- 外观模式
外观模式是一种结构型设计模式,它可以让你将复杂的子系统或组件封装成一个简化的接口,从而更容易地使用和管理这些子系统或组件。
首先,我们定义一个外观类Facade:
public class Facade {
private SubSystemA subSystemA;
private SubSystemB subSystemB;
private SubSystemC subSystemC;
public Facade() {
subSystemA = new SubSystemA();
subSystemB = new SubSystemB();
subSystemC = new SubSystemC();
}
public void operationA() {
subSystemA.operationA();
}
public void operationB() {
subSystemB.operationB();
}
public void operationC() {
subSystemC.operationC();
}
}
其中,SubSystemA、SubSystemB和SubSystemC是三个具体的子系统或组件,它们分别实现了自己的operationA()、operationB()和operationC()方法。
接下来,我们在客户端代码中使用Facade类来调用这些子系统或组件的方法:
public class Client {
public static void main(String[] args) {
Facade facade = new Facade();
facade.operationA();
facade.operationB();
facade.operationC();
}
}
运行客户端程序,输出结果如下:
SubSystemA operationA
SubSystemB operationB
SubSystemC operationC
可以看到,客户端代码只需要调用Facade类的相应方法即可完成对子系统或组件的操作,而不需要直接操作具体的子系统或组件。这样,我们就实现了将复杂的子系统或组件封装成一个简化的接口的目的。
- 享元模式
享元模式(Flyweight Pattern)是一种用于提高系统性能的设计模式,主要用于减少创建对象的数量,以减少内存占用和提高性能。它主要用于处理大量相似对象的情况,通过共享对象来避免大量的内存消耗。
下面是一个简单的 Java 实现例子:
interface Shape {
void draw();
}
class Circle implements Shape {
private String color;
private int x;
private int y;
private int radius;
public Circle(String color, int x, int y, int radius) {
this.color = color;
this.x = x;
this.y = y;
this.radius = radius;
}
@Override
public void draw() {
System.out.println("Circle: Draw() [Color : " + color + ", x : " + x + ", y :" + y + ", radius :" + radius);
}
}
class ShapeFactory {
private static final HashMap<String, Shape> circleMap = new HashMap<>();
public static Shape getCircle(String color) {
Circle circle = (Circle) circleMap.get(color);
if (circle == null) {
circle = new Circle(color, 0, 0, 10);
circleMap.put(color, circle);
System.out.println("Creating circle of color : " + color);
}
return circle;
}
}
public class FlyweightPatternDemo {
public static void main(String[] args) {
Shape redCircle = ShapeFactory.getCircle("Red");
redCircle.draw();
Shape greenCircle = ShapeFactory.getCircle("Green");
greenCircle.draw();
Shape blueCircle = ShapeFactory.getCircle("Blue");
blueCircle.draw();
}
}
在这个例子中,我们创建了一个Shape
接口和实现了Shape
接口的实体类Circle
。接下来,我们创建了一个ShapeFactory
类,该类使用了HashMap
存储Circle
对象。ShapeFactory
提供了一个静态方法getCircle()
,这个方法接受一个颜色字符串作为参数,然后返回一个对应颜色的Circle
对象。如果HashMap
中没有对应颜色的Circle
对象,那么就创建一个新的Circle
对象,并将其添加到HashMap
中。这就是享元模式的核心思想:重用已经创建的对象,而不是每次都创建一个新的对象。
- 代理模式