WormholeStack - Java编程
type: channel
link: https://www.wormholestack.com/tag/java/
items: Java程序执行本质
items: 你了解JDK SPI吗？
items: 你了解在Java中如何自定义注解嘛？
items: 如何理解java代理模式？
items: 你真的了解String的创建吗？
items: ArrayList是线程安全的吗？

Java程序执行本质
type: item
date: 2022-11-27T22:07:00+08:00
description: 文章已收录至精进Java编程系列 此系列其它文章 https://www.wormholestack.com/tag/java/0. 序本文开始前，提出几个问题，各位读者可以试着答答看。计算机是如何执行程序代码的？Java是编译型语言还是解释型语言呢？Java代码的执行过程是怎么样的？如果你对此还有些模糊，不妨看完本文。作为许多知识底层原理的基础。本文将从计算机发展史说起，探究计算机执行程序的过程，并浅析Java程序执行原理，以开篇的几个常见问题为引，来达到巩固Java基础的目的。只有足够了解程序本质，才会在编写代码的时得心应手。1.计算机发展史想了解计算机是如何程序的，我们首先应该对计算机有足够的了解。让我们了解计算机的发展，走进历史。1.1 图灵机在 1936 年，艾伦·图灵在论文《论可计算数及其在可判定性问题上的应用》中开创性地提出了计算机的通用逻辑模型 —— 图灵机模型。这一理论深刻地影响了第一代计算机科学家，更多能够实现计算功能的计算机被制造出来，图灵也因此被誉为 “计算机科学之父”。图灵的基本思想是用机器来模拟人们用纸笔进行数学运算的过程。下图为图灵机的简单示意图图灵机的基本组成如下：假设有一条无穷的【纸带】，纸带由一个个连续的格子组成，每个格子可以写入字符，纸带就好比内存，而纸带上的格子的字符就好比内存中的数据或程序；有一个【读写头】，读写头可以读取纸带上任意格子的字符，也可以把字符写入到纸带的格子，同时可以编辑数据（写入或擦除），移动纸带向左或者向右；读写头上有一些部件，比如存储单元、控制单元以及运算单元：存储单元用于存放数据；控制单元用于识别字符是数据还是指令，以及控制程序的流程等；运算单元用于执行运算指令；知道了图灵机的组成后，我们以简单数学运算的 1 + 2 作为例子，来看看它是怎么执行这行代码的。首先，用读写头把 【1、2、+】这 3 个字符分别写入到纸带上的 3 个格子，然后读写头先停在 1 字符对应的格子上；接着，读写头读入 1 到存储设备中，这个存储设备称为图灵机的状态；然后读写头向右移动一个格，用同样的方式把 2 读入到图灵机的状态，于是现在图灵机的状态中存储着两个连续的数字， 1 和 2；读写头再往右移动一个格，就会碰到 + 号，读写头读到 + 号后，将 + 号传输给【控制单元】，控制单元发现是一个 + 号而不是数字，所以没有存入到状态中，因为 + 号是运算符指令，作用是加和目前的状态，于是通知【运算单元】工作。运算单元收到要加和状态中的值的通知后，就会把状态中的 1 和 2 读入并计算，再将计算的结果 3 存放到状态中；最后，运算单元将结果返回给控制单元，控制单元将结果传输给读写头，读写头向右移动，把结果 3 写入到纸带的格子中；通过上面的图灵机计算 1 + 2 的过程，可以发现图灵机主要功能就是读取纸带格子中的内容，然后交给控制单元识别字符是数字还是运算符指令，如果是数字则存入到图灵机状态中，如果是运算符，则通知运算符单元读取状态中的数值进行计算，计算结果最终返回给读写头，读写头把结果写入到纸带的格子中。事实上，图灵机这个看起来很简单的工作方式，但它可以模拟计算机的任何算法，无论这个算法有多复杂。1.2 计算机的发展史随着计算机的元器件从继电器升级到电子管（也叫真空管），计算机也从 “机电” 时代进入到 “电子” 时代。第一台电子计算机诞生了，它由各种门电路组成的，这些门电路通过组装出一个固定的电路板，来执行一个特定的程序，也就与其他早期的计算机一样，都是不可重新编程的也就是说一台计算机只能执行一个特定的程序，如果需要修改程序功能，就需要将整个计算器拆开，然后重新组装电路。1943 年，Colossus Mark I 计算机（巨人 1 号）在英国 Bletchley 公园（二战时的密码破译机构）被建造出来，以破解纳粹通信，好家伙一口气造了 10 台。 Colossus Mark I 被认为是第一台可编程的电子计算机，编程方法就是使用大量的开关和插线板（PlugBoards）。 但 Colossus Mark I 并不是通用计算机，它只被设计用于执行密码分析相关的计算。直到 1946 年，John Mauchly 和 J. Presper Eckert 在美国宾夕法尼亚大学建造了 ENIAC（Electronic Numerical Integrator and Computer，中译：埃尼亚克）， ENIAC 被认为是第一台可编程的通用电子计算机，也被认为是第一台现代意义上的计算机。 但是，ENIAC 和 Colossus Mark I 一样都使用插线板编程，虽然不需要拆掉整台计算机来重新编程，但是编程效率依然非常低，据说一个简单程序在 ENIAC 上编程最多要花费三个星期。这对于早期异常…

你了解JDK SPI吗？
type: item
date: 2021-01-04T19:34:00+08:00
description: 什么是JDK SPI 机制？SPI (Service Provider Interface) 是一种将服务接口与服务实现分离以达到解耦、大大提升了程序可扩展性的机制。引入服务提供者就是引入了spi接口的实现者，通过本地的注册发现获取到具体的实现类，轻松可插拔。当服务的提供者提供了一种接口的实现之后，需要在 Classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件，此文件记录了该 jar 包提供的服务接口的具体实现类。当某个应用引入了该 jar 包且需要使用该服务时，JDK SPI 机制就可以通过查找这个 jar 包的 META-INF/services/ 中的配置文件来获得具体的实现类名，进行实现类的加载和实例化，最终使用该实现类完成业务功能。为什么要引入JDK SPI？上述已经描述清楚，在Java中SPI是被用来设计给服务提供商做插件用的，基于策略模式，来实现动态加载机制。例如我们在程序中只定义一个接口（规范），而具体的实现交给不同的服务提供者，在程序的启动中，读取设置好的配置文件，由配置文件决定加载哪一种服务的实现。这种将服务接口与实现分离达到解耦的作用，同时也提升了程序的可扩展性。举例说明：使用Java语言访问数据库的时候我们会使用到Java.sql.Driver接口不同数据库产品底层的协议不同，提供的Driver实现也不同，而开发者也不确定用户会选择哪种数据库，所以这种情况下可以使用JDK SPI机制，根据配置文件 用户选择哪种数据库，就调用哪种数据库的实现。 简单实现写一个简单的例子有一个接口Log，定义一个方法log，作用：作为日志输出传入参数。而实现接口的方式有两种，分别是Log4j和Logback。具体要使用哪种方式看配置文件。就这样，一个小例子下面是代码Log接口public interface Log { void log(String info); }两个实现类public class Logback implements Log { @Override public void log(String info) { System.out.println("Logback:" + info); } } public class Log4j implements Log { @Override public void log(String info) { System.out.println("Log4jLog4j:" + info); } } service配置文件service.impl.Log4j service.impl.Logback测试类public class Main { public static void main(String[] args) throws IOException { ServiceLoader<Log> spiLoader = ServiceLoader.load(Log.class); Iterator<Log> iteratorSpi = spiLoader.iterator(); while (iteratorSpi.hasNext()) { Log log = iteratorSpi.next(); log.log("MRyan"); } } }输出：源码分析由 ServiceLoader的load方法作为入口进行分析 传入的参数是接口类 public static ServiceLoader load(Class service) { //获取当前线程绑定的ClassLoader ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }接着调用重载load（）传入接口类和，当前线程绑定的ClassLoader public static ServiceLoader load(Class service, ClassLoader loader){ //为给定的服务类型和类加载器创建一个新的服务加载器。 return new ServiceLoader<>(service, loader); } // 表示正在加载的服务的类或接口 private final Class service; // 用于查找，加载和实例化提供程序的类加载器 private final ClassLoader loader; // 创建ServiceLoader时采取的访问控制上下文 private final AccessControlContext acc; private Service…

你了解在Java中如何自定义注解嘛？
type: item
date: 2020-12-17T09:41:00+08:00
description: 由一个小例子引出今天的课题：校验用户信息，后端对接收的对象的信息例如用户名和密码做一个指定的校验。代码如下：User类指定用户名和密码设定要求@Data @NoArgsConstructor @AllArgsConstructor public class User { /** * 用户名 不能为空 长度不能大于6 */ private String username; /** * 密码 不能为空 长度不能低于5 大于13 */ private String password; } 然后我们编写校验逻辑 /** * 模拟接收（username）用户名 （passwoord）密码 验证用户信息时候符合要求 */ public static boolean saveUser(User user) { //User为空返回false if (user == null) { System.out.println("失败：不可为空"); return false; } String username = user.getUsername(); String password = user.getPassword(); if (username == null || "".equals(username)) { System.out.println("｛用户名可空校验｝失败：不可为空"); return false; } else { System.out.println("｛用户名可空校验｝通过！！！"); if (username.length() > 6) { System.out.println("｛用户名长度校验｝失败：长度不能超过6"); return false; } else { System.out.println("｛用户名长度校验｝通过！！！"); } } if (password == null || "".equals(password)) { System.out.println("｛密码可空校验｝失败：不可为空"); return false; } else { System.out.println("｛密码可空校验｝通过"); if (password.length() < 5 || password.length() > 13) { System.out.println("｛密码长度校验｝失败：长度不能低于5，不能超过13"); return false; } else { System.out.println("｛用户名长度校验｝通过！！！"); } } return true; }测试类调用 public static void main(String[] args) { System.out.println(saveUser(new User("MRyan", "123456"))); System.out.println(saveUser(new User("", "123456"))); System.out.println(saveUser(new User("MRyan", "1234566666666666"))); System.out.println(saveUser(new User("MRyan", "12"))); }输出log查看结果｛用户名可空校验｝通过！！！ ｛用户名长度校验｝通过！！！ ｛密码可空校验｝通过 ｛用户名长度校验｝通过！！！ true ｛用户名可空校验｝失败：不可为空 false ｛用户名可空校验｝通过！！！ ｛用户名长度校验｝通过！！！ ｛密码可空校验｝通过 ｛密码长度校验｝失败：长度不能低于5，不能超过13 false ｛用户名可空校验｝通过！！！ ｛用户名长度校验｝通过！！！ ｛密码可空校验｝通过 ｛密码长度校验｝失败：长度不能低于5，不能超过13 false Process finished with exit code 0 实现了这个需求，但是我们会发现，校验融入了业务逻辑中，如果想要修改校验需求那就需要改动业务代码，这样做显然不是我们想要的。我们可以用Java注解的方式来完成校验。什么是Java注解1.注解的定义：Java文件叫做Annotation，用@interface表示。2.元注解：@interface上面按需要注解上一些东西，包括@Retention、@Target、@Document、@Inherited四种。3.注解的保留策略：@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Re…

如何理解java代理模式？
type: item
date: 2020-08-17T09:01:00+08:00
description: 需求假设现在有一个需求：在项目现有所有类的方法前后打印日志。你如何在不修改已有代码的前提下，如何完成这个需求？直切主题那这个时候我们可以使用动态代理要理解动态代理首先要理解代理模式什么是代理模式？ 代理模式给某一个对象提供一个代理对象，并由代理对象控制对原对象的引用。通俗的来讲代理模式就是我们生活中常见的中介。举个例子来说明：假如说我现在想买一辆二手车，虽然我可以自己去找车源，做质量检测等一系列的车辆过户流程，但是这确实太浪费我得时间和精力了。我只是想买一辆车而已为什么我还要额外做这么多事呢？于是我就通过中介公司来买车，他们来给我找车源，帮我办理车辆过户流程，我只是负责选择自己喜欢的车，然后付钱就可以了。用图表示如下：下面用代码来举一个例子说明假设有一个打印机的类public class Printer { public void print(){ System.out.println("打印!"); } }我想在打印之前先记录一下日志怎么做？最简单的方法：在打印的功能前面直接加上记录日志的功能。public class Printer { public void print(){ System.out.println("记录日志！"); System.out.println("打印！"); } }看上去好像没有问题，但是我们修改了打印机的源代码，破坏了面向对象的开闭原则，有可能影响到其它功能。怎么解决呢？很容易可以想到，既然不能修改原来的代码，那我新建一个类吧。public class LogPrinter extends Printer { public void print(){ System.out.println("记录日志！"); System.out.println("打印！"); } }这个类继承了打印机的类，重写了打印机的print方法，提供了记录日志的功能，以后需要打印机的时候使用这个类就好。问题似乎得到了解决，我们可以在这个解决方案的基础上进一步的优化：先抽象出一个接口:public interface IPrinter { void print(); }打印机类实现这个接口:public class Printer implements IPrinter { public void print(){ System.out.println("打印！"); } }创建打印机代理类也实现该接口，在构造函数中将打印机对象传进去，实现接口的打印方法时调用打印机对象的打印方法并在前面加上记录日志的功能:public class PrinterProxy implements IPrinter { private IPrinter printer; public PrinterProxy(){ this.printer = new printer(); } @Override public void print() { System.out.println("记录日志"); printer.print(); } }测试一下：public class Test { public static void main(String[] args) { PrinterProxy proxy = new PrinterProxy(); proxy.print(); } }结果出来了：记录日志打印以后我们就可以直接实例化PrinterProxy对象调用它的打印方法了，这就是静态代理模式，通过抽象出接口让程序的扩展性和灵活性更高了。静态代理是完美无缺的吗？考虑一下，如果我的打印机类中还有别的方法，也需要加上记录日志的功能，就不得不将记录日志的功能写n遍。进一步如果我还有电视机，电冰箱的类里面的所有方法也需要加上记录日志的功能，那要重复的地方就更多了。怎么办？动态代理闪亮登场：要想不重复写记录日志的功能，针对每一个接口实现一个代理类的做法肯定不可行了，可不可以让这些代理类的对象自动生成呢？JDK提供了invocationHandler接口和Proxy类，借助这两个工具可以达到我们想要的效果。invocationHandler接口上场：//Object proxy:被代理的对象 //Method method:要调用的方法 //Object[] args:方法调用时所需要参数 public interface InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable; }接口里只有一个方法invoke，这个方法非常重要，先混个脸熟，稍后解释。Proxy类上场，它里面有一个很重要的方法 newP…

你真的了解String的创建吗？
type: item
date: 2020-08-11T17:54:00+08:00
description: 起因在文章的开始之前，有一个问题需要思考。String s = "MRyan";String s = new String("MRyan");以上是String的两种赋值方式，它们有什么区别吗？它们在内存中有几个实例？存储在哪个区域里？实例存储在哪里？字面量存储在哪里？想要回答这些问题，需要对JVM有一定的了解狂补JVM基础知识都知道JVM的内存结构包括堆，虚拟机栈，方法区，程序计数器，本地方法栈。其中和本文章有关系的1. 堆：作为整个JVM内存结构中占用最大的一块空间，存放对象实例和数组，它是线程共享的。2. 虚拟机栈：在线程的生命周期中，参与计算的数据会频繁的入栈和出栈，栈的生命周期和线程一样，栈内的每条数据都是栈帧。在每个java方法被调用的时候，就会创建一个栈帧并入栈，执行完调用在出栈。栈帧包含：局部变量表，操作数栈，动态链接，返回地址。它是线程独占的 。3. 方法区：又称非堆区，用于存储JVM加载的类型信息，常量，静态变量，代码缓存等数据。在JDK1.7后用元空间替代永久代，随之字符串常量池和静态变量移动到了堆中。它是线程共享的。其中在JDK1.7以后包括常量池，运行时常量池，字符串常量池都由方法区移动到了堆内存中。常量池：即class文件常量池，是class文件的一部分，用于保存编译时确定的数据。包括字面量和符号引用。运行时常量池：它是每个类私有的。每个class文件里的“常量池”在类被加载器加载之后，常量池中的数据就通过映射存放在运行时常量池，由于Java语言并不要求常量一定只能在编译期产生，运行期间也可能产生新的常量，这些常量被放在运行时常量池中，这里所说的常量包括：基本类型包装类（包装类不管理浮点型，整形只会管理-128到127）和String（也可以通过String.intern()方法可以强制将String放入常量池）。字符串常量池：HotSpot VM里，记录interned string的一个全局表叫做StringTable，它本质上就是个HashSet。注意它只存储对java.lang.String实例的引用，而不存储String对象的内容。有了以上知识点，我们可以继续说回String了回归正题编写一段代码如下：package com.mryan.csdn.string; class Test{ public static void main(String[] args){ String s1= "MRyan"; } }声明了一个字面量为“MRyan”的字符串，因为由于String的不可变性（被final修饰）所以字符串"MRyan"作为字面量存储在class文件常量池中。类加载之后class文件里常量池里大部分数据会被加载到运行时常量池，但是String类型的数据的引用会被同时存在字符串常量池中，实例创建在堆中。而其实这只是Test类被类加载的时候，并未有启动程序线程，此时“MRyan”的引用已经存放在字符串常量池中，实例存放在堆中。等主线程开始创建s1变量时，JVM就会到字符串常量池里找，看有没有能equals("MRyan")的String。如果找到了，就在虚拟机栈中当前栈帧的局部变量表里创建s1变量，然后把字符串常量池里对MRyan对象的引用复制给s1变量。找不到的话，才会在heap堆重新创建一个对象，然后把引用驻留到字符串常量区。然后再把引用复制栈帧的局部变量表。画个简图：当定义了多个值为"MRyan"的String，情况是什么样的呢？package com.mryan.csdn.string; class Test{ public static void main(String[] args){ String s1= "MRyan"; String s2= "MRyan"; String s3= "MRyan"; System.out.println(s1==s2); System.out.println(s2==s3); } }输出：和刚才一样，其实是局部变量表里三个变量统一指向同一个堆内存地址。画个简图：但如果是用new关键字来创建字符串，情况就不一样了package com.mryan.csdn.string; class Test{ public static void main(String[] args){ String s1= "MRyan"; String s2= "MRyan"; String s3= new String("MRyan"); System.out.println(s1==s2); System.out.println(s2==s3); } }输出：这时候，s1和s2还是和之前一样。但s3不同因为new关键字会在Heap堆申请一块全新的内存，来创建新对象…

ArrayList是线程安全的吗？
type: item
date: 2020-06-14T09:24:00+08:00
description: 问题介绍List如何在增加元素的同时删除元素。这还不简单？直接上代码： List<String> lists = new ArrayList<>(); lists.add("MRyan"); lists.add("MRyan2"); lists.add("MRyan3"); for (String list : lists) { if (list.equals("MRyan")) { lists.remove(list); } } System.out.println(lists); 然后兴奋的运行程序，结果发现Exception 。很惨，报错了，报了如下错误：Exception in thread "main" java.util.ConcurrentModificationException at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1012) at java.base/java.util.ArrayList$Itr.next(ArrayList.java:966) at com.text.text.main(text.java:18) 报错原因及分析源码发生了什么错误？java.util.ConcurrentModificationException表示ArrayList在迭代的时候如果同时对其进行修改就会抛出的异常。也就是说这是一个并发的问题。那为什么会发生这个错误？接下来我们来从源码下手：定位到ArrayList的源码查看ArrayList的add方法： /** * Appends the specified element to the end of this list. * * @param e element to be appended to this list * @return {@code true} (as specified by {@link Collection#add}) */ public boolean add(E e) { modCount++; add(e, elementData, size); return true; } 首先modCount进行了自增然后调用了自己的private void add（E e, Object[] elementData, int s）方法。定位到private void add（E e, Object[] elementData, int s）方法 /** * This helper method split out from add(E) to keep method * bytecode size under 35 (the -XX:MaxInlineSize default value), * which helps when add(E) is called in a C1-compiled loop. */ private void add(E e, Object[] elementData, int s) { if (s == elementData.length) elementData = grow(); elementData[s] = e; size = s + 1; }其实就是首先判断是否需要扩容，然后将值添加到数组中，数组大小加1。那你会有疑问了，刚才出现的modCount是什么？别着急往下看定位到remove（Object o）方法： /** * Removes the first occurrence of the specified element from this list, * if it is present. If the list does not contain the element, it is * unchanged. More formally, removes the element with the lowest index * {@code i} such that * {@code Objects.equals(o, get(i))} * (if such an element exists). Returns {@code true} if this list * contained the specified element (or equivalently, if this list * changed as a result of the call). * * @param o element to be removed from this list, if present …