• [技术干货] JavaScript 数据结构详解
    最近在复习JavaScript的基础知识,和第一次学确实有了很不一样的感受,第一次学的比较浅,但是回头再进行学习的时候,发现有很多遗漏的东西,所以今天想分享一下新学到的知识,后面会一点一点补充更新JavaScript的数据结构有8个,分别是number string boolean object undefined null 还有es6新增的symbol和bigint,今天主要分享一下null undefined number,其他的等复习完会及时更新的null:null是一个独立的数据类型,表示一个空值或者是一个对象没有值null有几个特殊的用法,操作如下:1.当使用Number方法来识别null的时候,输出为0 console.log(Number(null))  //02.对null实现一些运算符操作(可以将null当做0来进行计算) console.log(2 + null) //2 console.log(2 * null) //0undefined:undefined比较特殊,表示未定义,比如你在生命一个变量,但是没给他赋值的时候,然后检测该变量的类型,输出就是undefined let a console.log(a) //undefined 1.当使用Number方法来识别undefined的时候,输出为NaNconsole.log(Number(undefined)) //NaN2.对undefined实现一些运算符操作(可以将undefined当做没有值来进行计算) console.log(undefined + 2) //NaN console.log(undefined * 2) //NaN 3.当使用undefined和null来进行比较的时候,非严格模式下,两者是相等的console.log(null == undefined) //trueconsole.log(null === undefined) //falsenumber:number是用来表示整数和浮点数已经NaN的数据类型,JavaScript的底层没有整数1.所有的数字都是使用64位浮点数来进行存储console.log(1 === 1.0) //true2.当小数在进行相加的时候,具有误差console.log((0.3 - 0.2) === 0.1) //false3.当一个计算的数大于2的53次方,计算就不准确了console.log(Math.pow(2, 53) === Math.pow(2, 53) + 1) //true4.当一个数大于2的1024次方,就会溢出,如果小于2的-1075次方,会溢出为0console.log(Math.pow(2, 1024)) //Infinityconsole.log(Math.pow(2, -1075) ) //05.+0和-0在很多情况下+0和-0是一样的,但是只有当他们表示分母的时候,会有不一样的结果console.log(+0 === -0) //trueconsole.log(1 / +0 === 1 / -0) //false6.NaN表示number类型,当对NaN进行幂运算的时候,输出为1,其他情况下都为NaNconsole.log(NaN ** 0) //17.进制十进制表示没有前导0的数值,二进制前缀(0b/0B),八进制前缀(0o/0O),十六进制前缀(0x/0X),特殊情况: 有前导0的数值会被视为八进制,但是如果前导0后面有数字8和9,则该数值被视为十进制。console.log(099) //99console.log(088) //88console.log(077) //63 8.infinity运算1. 范围:Infinity大于一切数值(除了NaN),-Infinity小于一切数值(除了NaN)。console.log(Infinity > -100) //trueconsole.log(-Infinity < -100) //falseconsole.log(Infinity > NaN) //falseconsole.log(-Infinity < NaN) //false2. Infinity与undefined计算,返回的都是NaN。console.log(Infinity + undefined) //NaNconsole.log(Infinity - undefined) //NaNconsole.log(Infinity * undefined) //NaNconsole.log(Infinity / undefined) //NaN3. Infinity减去或除以Infinity,得到NaN。console.log(Infinity - Infinity) //NaNconsole.log(Infinity / Infinity) //NaN4.0乘以Infinity,返回NaN;0除以Infinity,返回0;Infinity除以0,返回Infinity。console.log(0 * Infinity) //NaNconsole.log(0 / Infinity) //0console.log(Infinity / 0) //Infinity5.Infinity与null计算时,null会转成0,等同于与0的计算。只用相乘的时候,返回NaNconsole.log(Infinity + null) //Infinityconsole.log(Infinity - null) //Infinityconsole.log(Infinity * null) //NaNconsole.log(Infinity / null) ————————————————原文链接:https://blog.csdn.net/2301_81253185/article/details/148673464
  • [技术干货] 【Java 开发日记】你会不会使用 SpringBoot 整合 Flowable 快速实现工作流呢?
    1、流程引擎介绍Flowable 是一个使用 Java 编写的轻量级业务流程引擎。Flowable 流程引擎可用于部署 BPMN2.0 流程定义(用于定义流程的行业 XML 标准),创建这些流程定义的流程实例,进行查询,访问运行中或历史的流程实例与相关数据,等等。Java 领域另一个流程引擎是 Activiti,不这两个东西只要会使用其中一个,另一个就不在话下。咱就不废话了,上代码吧。 2、创建项目首先创建一个 Spring Boot 项目,引入 Web、和 MySQL 驱动两个依赖,如下图:项目创建成功之后,引入 flowable 依赖,如下:<dependency>    <groupId>org.flowable</groupId>    <artifactId>flowable-spring-boot-starter</artifactId>    <version>6.7.2</version></dependency>这个会做一些自动化配置,默认情况下,所以位于 resources/processes 的流程都会被自动部署。接下来在 application.yaml 中配置一下数据库连接信息,当项目启动的时候会自动初始化数据库,将来流程引擎运行时候的数据会被自动持久化到数据库中。spring:  datasource:    username: root    password: 123    url: jdbc:mysql:///flowable?serverTimezone=Asia/Shanghai&useSSL=false配置完成后,就可以启动项目了。项目启动成功之后,flowable 数据库中就会自动创建如下这些表,将来流程引擎相关的数据都会自动保存到这些表中。默认的表比较多,截图只是其中一部分。 3、画流程图画流程图算是比较有挑战的一个步骤了,也是流程引擎使用的关键。官方提供了一些流程引擎绘制工具,感兴趣的小伙伴可以自行去体验;IDEA 也自带了一个流程可视化的工具,但是特别难用。这里说一下常用的 IDEA 插件 Flowable BPMN visualizer,如下图:装好插件之后,在 resources 目录下新建 processes 目录,这个目录下的流程文件将来会被自动部署。接下来在 processes 目录下,新建一个 BPMN 文件(插件装好了就有这个选项了),如下:来画个请假的流程,就叫做 ask_for_leave.bpmn20.xml,注意最后面的 .bpmn20.xml 是固定后缀。文件创建出来之后,右键单击,选择 View BPMN(Flowable) Diagram,就打开了可视化页面了,就可以来绘制自己的流程图了。请假流程画出来是这样:员工发起一个请假流程,首先是组长审核,组长审核通过了,就进入到经理审核,经理审核通过了,这个流程就结束了,如果组长审核未通过或者经理审核未通过,则流程给员工发送一个请假失败的通知,流程结束。来看下这个流程对应的 XML 文件,一些流程细节会在 XML 文件中体现出来,如下:<process id="ask_for_leave" name="ask_for_leave" isExecutable="true">    <userTask id="leaveTask" name="请假" flowable:assignee="#{leaveTask}"/>    <userTask id="zuzhangTask" name="组长审核" flowable:assignee="#{zuzhangTask}"/>    <userTask id="managerTask" name="经理审核" flowable:assignee="#{managerTask}"/>    <exclusiveGateway id="managerJudgeTask"/>    <exclusiveGateway id="zuzhangJudeTask"/>    <endEvent id="endLeave" name="结束"/>    <startEvent id="startLeave" name="开始"/>    <sequenceFlow id="flowStart" sourceRef="startLeave" targetRef="leaveTask"/>    <sequenceFlow id="modeFlow" sourceRef="leaveTask" targetRef="zuzhangTask"/>    <sequenceFlow id="zuzhang_go" sourceRef="zuzhangJudeTask" targetRef="managerTask" name="通过">        <conditionExpression xsi:type="tFormalExpression"><![CDATA[${checkResult=='通过'}]]></conditionExpression>    </sequenceFlow>    <sequenceFlow id="zuzhang_reject" sourceRef="zuzhangJudeTask" targetRef="sendMail" name="拒绝">        <conditionExpression xsi:type="tFormalExpression"><![CDATA[${checkResult=='拒绝'}]]></conditionExpression>    </sequenceFlow>    <sequenceFlow id="jugdeFlow" sourceRef="managerTask" targetRef="managerJudgeTask"/>    <sequenceFlow id="flowEnd" name="通过" sourceRef="managerJudgeTask" targetRef="endLeave">        <conditionExpression xsi:type="tFormalExpression"><![CDATA[${checkResult=='通过'}]]></conditionExpression>    </sequenceFlow>    <sequenceFlow id="rejectFlow" name="拒绝" sourceRef="managerJudgeTask" targetRef="sendMail">        <conditionExpression xsi:type="tFormalExpression"><![CDATA[${checkResult=='拒绝'}]]></conditionExpression>    </sequenceFlow>    <serviceTask id="sendMail" flowable:exclusive="true" name="发送失败提示" isForCompensation="true" flowable:class="org.javaboy.flowable.AskForLeaveFail"/>    <sequenceFlow id="endFlow" sourceRef="sendMail" targetRef="askForLeaveFail"/>    <endEvent id="askForLeaveFail" name="请假失败"/>    <sequenceFlow id="zuzhangTask_zuzhangJudeTask" sourceRef="zuzhangTask" targetRef="zuzhangJudeTask"/></process>结合 XML 文件来解释一下这里涉及到的 Flowable 中的组件:<process> :表示一个完整的工作流程。<startEvent> :工作流中起点位置,也就是图中的绿色按钮。<endEvent> :工作流中结束位置,也就是图中的红色按钮。<userTask> :代表一个任务审核节点(组长、经理等角色),这个节点上有一个 flowable:assignee 属性,这表示这个节点该由谁来处理,将来在 Java 代码中调用的时候,需要指定对应的处理人的 ID 或者其他唯一标记。<serviceTask>:这是服务任务,在具体的实现中,这个任务可以做任何事情。<exclusiveGateway> :逻辑判断节点,相当于流程图中的菱形框。<sequenceFlow> :链接各个节点的线条,sourceRef 属性表示线的起始节点,targetRef 属性表示线指向的节点,图中的线条都属于这种。流程图这块松哥和大家稍微说一下,咋一看这个图挺复杂很难画,但是实际上只要认认真真去捋一捋这里边的各个属性,基本上很快就明白到底是怎么一回事。 4、开发接口接下来写几个接口,来体验一把流程引擎。在正式体验之前,先来熟悉几个类,这几个类一会写代码会用到。 4.1 Java 类梳理ProcessDefinition这个最好理解,就是流程的定义,也就相当于规范,每个 ProcessDefinition 都会有一个 id。 ProcessInstance这个就是流程的一个实例。简单来说,ProcessDefinition 相当于是类,而 ProcessInstance 则相当于是根据类 new 出来的对象。 ActivityActivity 是流程标准规范 BPMN2.0 里面的规范,流程中的每一个步骤都是一个 Activity。 ExecutionExecution 的含义是流程的执行线路,通过 Execution 可以获得当前 ProcessInstance 当前执行到哪个 Activity了。 TaskTask 就是当前要做的工作。实际上这里涉及到的东西比较多,不过这里先整一个简单的例子,所以上面这些知识点暂时够用了。 4.2 查看流程图在正式开始之前,先准备一个接口,用来查看流程图的实时执行情况,这样方便查看流程到底执行到哪一步了。具体的代码如下:@RestControllerpublic class HelloController {    @Autowired    RuntimeService runtimeService;    @Autowired    TaskService taskService;    @Autowired    RepositoryService repositoryService;    @Autowired    ProcessEngine processEngine;    @GetMapping("/pic")    public void showPic(HttpServletResponse resp, String processId) throws Exception {        ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(processId).singleResult();        if (pi == null) {            return;        }        List<Execution> executions = runtimeService                .createExecutionQuery()                .processInstanceId(processId)                .list();        List<String> activityIds = new ArrayList<>();        List<String> flows = new ArrayList<>();        for (Execution exe : executions) {            List<String> ids = runtimeService.getActiveActivityIds(exe.getId());            activityIds.addAll(ids);        }        /**         * 生成流程图         */        BpmnModel bpmnModel = repositoryService.getBpmnModel(pi.getProcessDefinitionId());        ProcessEngineConfiguration engconf = processEngine.getProcessEngineConfiguration();        ProcessDiagramGenerator diagramGenerator = engconf.getProcessDiagramGenerator();        InputStream in = diagramGenerator.generateDiagram(bpmnModel, "png", activityIds, flows, engconf.getActivityFontName(), engconf.getLabelFontName(), engconf.getAnnotationFontName(), engconf.getClassLoader(), 1.0, false);        OutputStream out = null;        byte[] buf = new byte[1024];        int legth = 0;        try {            out = resp.getOutputStream();            while ((legth = in.read(buf)) != -1) {                out.write(buf, 0, legth);            }        } finally {            if (in != null) {                in.close();            }            if (out != null) {                out.close();            }        }    }}这就一个工具,没啥好说的,一会大家看完后面的代码,再回过头来看这个接口,很多地方就都懂了。 4.3 开启一个流程为了方便,接下来的代码都在单元测试中完成。首先来开启一个流程,代码如下:String staffId = "1000";/** * 开启一个流程 */@Testvoid askForLeave() {    HashMap<String, Object> map = new HashMap<>();    map.put("leaveTask", staffId);    ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("ask_for_leave", map);    runtimeService.setVariable(processInstance.getId(), "name", "javaboy");    runtimeService.setVariable(processInstance.getId(), "reason", "休息一下");    runtimeService.setVariable(processInstance.getId(), "days", 10);    logger.info("创建请假流程 processId:{}", processInstance.getId());}首先由员工发起一个请假流程,map 中存放的 leaveTask 是在 XML 流程文件中提前定义好的,提前定义好当前这个任务创建之后,该由谁来处理,这里是假设由工号为 1000 的员工来发起这样一个请假流程。同时,还设置了一些额外信息。ask_for_leave 是在 XML 文件中定义的一个 process 的名称。好啦,现在执行这个单元测试方法,执行完成后,控制台会打印出当前这个流程的 id,拿着这个 id 去访问 4.2 小节的接口,结果如下:可以看到,请假用红色的框框起来了,说明当前流程走到了这一步。 4.4 将请求提交给组长接下来,就需要将这个请假流程向后推进一步,将请假事务提交给组长,代码如下:String zuzhangId = "90";/** * 提交给组长审批 */@Testvoid submitToZuzhang() {    //员工查找到自己的任务,然后提交给组长审批    List<Task> list = taskService.createTaskQuery().taskAssignee(staffId).orderByTaskId().desc().list();    for (Task task : list) {        logger.info("任务 ID:{};任务处理人:{};任务是否挂起:{}", task.getId(), task.getAssignee(), task.isSuspended());        Map<String, Object> map = new HashMap<>();        //提交给组长的时候,需要指定组长的 id        map.put("zuzhangTask", zuzhangId);        taskService.complete(task.getId(), map);    }}首先利用 staffId 查找到当前员工的 id,进而找到当前员工需要执行的任务,遍历这个任务,调用 taskService.complete 方法将任务提交给组长,注意在 map 中指定组长的 id。提交完成后,再去看流程图片,如下:可以看到,流程图走到组长审批了。 4.5 组长审批组长现在有两种选择,同意或者拒绝,同意的代码如下:/** * 组长审批-批准 */@Testvoid zuZhangApprove() {    List<Task> list = taskService.createTaskQuery().taskAssignee(zuzhangId).orderByTaskId().desc().list();    for (Task task : list) {        logger.info("组长 {} 在审批 {} 任务", task.getAssignee(), task.getId());        Map<String, Object> map = new HashMap<>();        //组长审批的时候,如果是同意,需要指定经理的 id        map.put("managerTask", managerId);        map.put("checkResult", "通过");        taskService.complete(task.getId(), map);    }}通过组长的 id 查询组长的任务,同意的话,需要指定经理,也就是这个流程下一步该由谁来处理。拒绝的代码如下:/** * 组长审批-拒绝 */@Testvoid zuZhangReject() {    List<Task> list = taskService.createTaskQuery().taskAssignee(zuzhangId).orderByTaskId().desc().list();    for (Task task : list) {        logger.info("组长 {} 在审批 {} 任务", task.getAssignee(), task.getId());        Map<String, Object> map = new HashMap<>();        //组长审批的时候,如果是拒绝,就不需要指定经理的 id        map.put("checkResult", "拒绝");        taskService.complete(task.getId(), map);    }}拒绝的话,就没那么多事了,直接设置 checkResult 为拒绝即可。假设这里执行了同意,那么流程图如下: 4.6 经理审批经理审批和组长审批差不多,只不过经理这里是最后一步了,不需要再指定下一位处理人了,同意的代码如下:/** * 经理审批自己的任务-批准 */@Testvoid managerApprove() {    List<Task> list = taskService.createTaskQuery().taskAssignee(managerId).orderByTaskId().desc().list();    for (Task task : list) {        logger.info("经理 {} 在审批 {} 任务", task.getAssignee(), task.getId());        Map<String, Object> map = new HashMap<>();        map.put("checkResult", "通过");        taskService.complete(task.getId(), map);    }}拒绝代码如下:/** * 经理审批自己的任务-拒绝 */@Testvoid managerReject() {    List<Task> list = taskService.createTaskQuery().taskAssignee(managerId).orderByTaskId().desc().list();    for (Task task : list) {        logger.info("经理 {} 在审批 {} 任务", task.getAssignee(), task.getId());        Map<String, Object> map = new HashMap<>();        map.put("checkResult", "拒绝");        taskService.complete(task.getId(), map);    }}4.7 拒绝流程如果组长拒绝了或者经理拒绝了,也有相应的处理方案,首先在 XML 流程文件定义时,如下:<serviceTask id="sendMail" flowable:exclusive="true" name="发送失败提示" isForCompensation="true" flowable:class="org.javaboy.flowable.AskForLeaveFail"/>如果请假被拒绝,会进入到这个 serviceTask,serviceTask 对应的处理类是 org.javaboy.flowable.AskForLeaveFail,该类的代码如下:public class AskForLeaveFail implements JavaDelegate {    @Override    public void execute(DelegateExecution execution) {        System.out.println("请假失败。。。");    }}也就是请假失败会进入到这个方法中,现在就可以在这个方法中该干嘛干嘛了。————————————————原文链接:https://blog.csdn.net/2402_87298751/article/details/148666102
  • [技术干货] JAVA SE 文件IO
    1. File类的使用这里主要介绍对文件增删改查的操作 , 不是对文件中的内容进行增删改查1.1 构造方法方法    说明File(File parent, String child)    根据⽗⽬录 + 孩⼦⽂件路径,创建⼀个新的 File 实例File(String pathname)    根据⽂件路径创建⼀个新的 File 实例,路径可以是绝对路径或者相对路径File(String parent, String child)    根据⽗⽬录 + 孩⼦⽂件路径,创建⼀个新的 File 实例,⽗⽬录⽤路径表⽰1.2 常用方法方法    说明getParent()    返回 File 对象的⽗⽬录⽂件路径getName()    返回 FIle 对象的纯⽂件名称getPath()    返回 File 对象的⽂件路径getAbsolutePath()    返回 File 对象的绝对路径getCanonicalPath()    返回 File 对象的修饰过的绝对路径exists()    判断 File 对象描述的⽂件是否真实存在isDirectory()    判断 File 对象代表的⽂件是否是⼀个⽬录isFile()    判断 File 对象代表的⽂件是否是⼀个普通⽂件createNewFile()    根据 File 对象,⾃动创建⼀个空⽂件。成功创建后返回 truedelete()    根据 File 对象,删除该⽂件。成功删除后返回 truedeleteOnExit()    根据 File 对象,标注⽂件将被删除,删除动作会到 JVM 运⾏结束时才会进⾏list()    返回 File 对象代表的⽬录下的所有⽂件名listFiles()    返回 File 对象代表的⽬录下的所有⽂件,以 File 对象表⽰mkdir()    创建 File 对象代表的⽬录mkdirs()    创建 File 对象代表的⽬录,如果必要,会创建中间⽬录renameTo(File dest)    进⾏⽂件改名,也可以视为我们平时的剪切、粘贴操作canRead()    判断⽤⼾是否对⽂件有可读权限canWrite()    判断⽤⼾是否对⽂件有可写权限2. I/O流I/O是Input/Output的缩写。I/O技术是非常实用的技术,用于处理数据传输。如读/写文件,网络通讯等。Java程序中,对于数据的输入/输出操作以"流(stream)" 的方式进行。java.io包下提供了各种"流"类和接口,用以获取不同种类的数据,并通过方法输入或输出数据2.1 I/O流的分类按操作数据单位不同分为:字节流(8 bit),字符流(16 bit)按数据流的流向不同分为:输入流,输出流按流的角色的不同分为:节点流,处理流抽象基类    字节流    字符流输入流    InputStream    Reader输出流    OutputStream    Writer2.2 I/O体系Java的io流共涉及40多个类,实际上非常规则,都是以上述4个抽象基类派生的3. 字节流3.1 InputStream类InputStream 是抽象类 , 我们现在只关心从文件中读取,所以使用 FileInputStream类实例化对象FileInputStream类的构造方法方法    说明FileInputStream(File file)    通过指定的File对象来创建输入流。FileInputStream(String pathname)    通过指定文件的路径字符串来创建输入流。FileInputStream类的常见方法方法    说明int read()    从输入流中读取一个字节的数据。int read(byte[] b)    从输入流中读取一定数量的字节到字节数组中,返回长度。int read(byte[] b, int off, int len)    最多读取 len - off 字节的数据到 b中,放在从 off 开始,返回实际读到的数量;-1 代表以及读完了void close()    关闭字节流3.2 OutputStream类OutputStream 同样只是一个抽象类,要使用还需要具体的实现类。我们现在还是只关心写入文件中,所以使用 FileOutputStream类实例化对象FileOutputStream的构造方法方法    说明FileOutputStream(File file)    通过指定的File对象来创建输出流FilenOutputStream(String name)    通过指定文件的路径字符串来创建输出流FileOutputStream的常用方法方法    说明write(int b)    写入一个字节到文件。write(byte[] b)    将一个字节数组写入文件write(byte[] b, int off, int len)    从字节数组的指定位置开始,写入指定长度的字节到文件close()    关闭输出流,释放相关资源。4. 字符流4.1 Reader类FileReader类主要用于从文件中读取字符数据。它是一个字符输入流,继承自InputStreamReader(转换流),抽象基类为Reader。可以通过构造方法传入文件路径来创建FileReader的构造方法方法    说明FileReader(File file)    创建一个与指定文件对象相关联的FileReader。FileReader(String fileName)    创建一个与指定文件路径名相关联的FileReaderFileReader的常用方法方法    说明int read()    从输入流中读取一个字符,返回该字符的整数表示(到达文件末尾返回 -1)int read(char[] cbuf)    将字符读入数组。返回读取的长度void close()    关闭该流并释放与之关联的所有资源。4.2 Writer类FileWriter类用于将字符数据写入文件。FileWriter的常用方法方法    说明write(int c)    写入单个字符到文件中。write(char[] cbuf)    将字符数组写入文件中。write(String str)    写入字符串到文件中。write(String str, int off, int len)    写入字符串的一部分到文件中。flush()    刷新缓冲区,将数据写入文件。close()    关闭文件并释放相关资源。————————————————原文链接:https://blog.csdn.net/2401_82690001/article/details/148137889
  • [技术干货] 修改trae全局默认的JDK版本
    找到setting.json文件通过Trae中的搜索功能,可以找到Trae所使用的配置文件。修改JDK版本前提:已经按照对应版本的JDK在setting.json文件中找到“java.import.gradle.java.home”和“metals.javaHome”两个配置项,改成你想要的JDK版本的路径即可。小技巧-快捷查找JDK路径可以在setting.json文件中搜索“terminal.integrated.profiles.osx”这个配置项,这个配置项中就是trae扫描到的本机中安装的JDK版本及路径,复制想要的JDK版本的路径,直接使用即可。————————————————原文链接:https://blog.csdn.net/u011924665/article/details/146208263
  • [技术干货] 【Java篇】一气化三清:类的实例化与封装的智慧之道
    五、对象的构造及初始化5.1 如何初始化对象通过前面知识点的学习我们知道,在Java方法内部定义一个局部变量时,必须要初始化,否则会编译失败。public static void main(String[] args) {int a;System.out.println(a);}// Error:(26, 28) java: 可能尚未初始化变量a要让上述代码通过编译,非常简单,只需在正式使用变量之前给它设置初始值即可。public static void main(String[] args) {    Date d = new Date();    d.printDate();    d.setDate(2021,6,9);    d.printDate();}// 代码可以正常通过编译如果是对象,就需要调用之前写的 setDate 方法将具体的日期设置到对象中。通过上述例子我们发现两个问题:问题1:每次对象创建好后调用 setDate 方法设置具体日期显得比较麻烦,那么对象该如何初始化?问题2:局部变量必须初始化才能使用,而字段声明之后没有给值依然可以使用,这是因为字段具有默认初始值。为了解决问题1,Java引入了 构造方法,使得对象在创建时就能完成初始化操作。5.2 构造方法构造方法(也称为构造器)是一种特殊的成员方法,其主要作用是初始化对象。5.2.1 构造方法的概念public class Date {    public int year;    public int month;    public int day;    // 构造方法:    // 名字与类名相同,没有返回值类型,设置为void也不行    // 一般情况下使用public修饰    // 在创建对象时由编译器自动调用,并且在对象的生命周期内只调用一次    public Date(int year, int month, int day){        this.year = year;        this.month = month;        this.day = day;        System.out.println("Date(int,int,int)方法被调用了");    }    public void printDate(){        System.out.println(year + "-" + month + "-" + day);    }    public static void main(String[] args) {        // 此处创建了一个Date类型的对象,并没有显式调用构造方法        Date d = new Date(2021,6,9); // 输出Date(int,int,int)方法被调用了        d.printDate(); // 2021-6-9    }}构造方法的特点是:名字必须与类名相同,且没有返回值类型(连void都不行)。在创建对象时由编译器自动调用,并且在对象的生命周期内只调用一次。注意:构造方法的作用是对对象中的成员进行初始化,并不负责给对象开辟内存空间。5.2.2 构造方法的特性构造方法具有如下特性:名字必须与类名完全相同没有返回值类型,即使设置为void也不行创建对象时由编译器自动调用,且在对象生命周期内只调用一次(就像人的出生,每个人只能出生一次)支持重载:同一个类中可以定义多个构造方法,只要参数列表不同即可示例代码1:带参构造方法public class Date {    public int year;    public int month;    public int day;        // 构造方法:名字与类名相同,没有返回值类型,使用public修饰    // 在创建对象时由编译器自动调用,并且在对象的生命周期内只调用一次    public Date(int year, int month, int day) {        this.year = year;        this.month = month;        this.day = day;        System.out.println("Date(int, int, int)方法被调用了");    }        public void printDate() {        System.out.println(year + "-" + month + "-" + day);    }        public static void main(String[] args) {        // 此处创建了一个Date类型的对象,并没有显式调用构造方法        Date d = new Date(2021, 6, 9); // 输出:Date(int, int, int)方法被调用了        d.printDate();                // 输出:2021-6-9    }}示例代码2:无参构造方法public class Date {    public int year;    public int month;    public int day;        // 无参构造方法:给成员变量设置默认初始值    public Date() {        this.year = 1900;        this.month = 1;        this.day = 1;    }}上述两个构造方法名字相同但参数列表不同,构成了方法的重载。如果用户没有显式定义构造方法,编译器会生成一个默认的无参构造方法。注意:一旦用户显式定义了构造方法,编译器就不会再生成默认构造方法了示例代码3:仅定义带参构造方法时默认构造方法不会生成// 带有三个参数的构造方法public Date(int year, int month, int day) {    this.year = year;    this.month = month;    this.day = day;}public void printDate(){    System.out.println(year + "-" + month + "-" + day);}public static void main(String[] args) {    Date d = new Date(); // 编译期报错,因为没有无参构造方法    d.printDate();}示例代码4:只有无参构造方法的情况public class Date {    public int year;    public int month;    public int day;        public void printDate(){        System.out.println(year + "-" + month + "-" + day);    }        public static void main(String[] args) {        Date d = new Date();        d.printDate();    }}示例代码5:只有带参构造方法的情况public class Date {    public int year;    public int month;    public int day;        public Date(int year, int month, int day) {        this.year = year;        this.month = month;        this.day = day;    }}构造方法中可以通过 this(…) 调用其他构造方法来简化代码。注意:this(…) 必须是构造方法中的第一条语句,否则编译器会报错。示例代码6:正确使用this(…)实现构造器链public class Date {    public int year;    public int month;    public int day;        // 无参构造方法 -- 内部调用带参构造方法实现初始化    // 注意:this(1900, 1, 1);必须是构造方法中的第一条语句    public Date(){        // System.out.println(year); // 若取消注释则编译会失败        this(1900, 1, 1);        // 以下赋值代码被省略,因为已在带参构造方法中完成初始化        // this.year = 1900;        // this.month = 1;        // this.day = 1;    }        // 带有三个参数的构造方法    public Date(int year, int month, int day) {        this.year = year;        this.month = month;        this.day = day;    }}注意:构造方法中的 this(…) 调用不能形成循环,否则会导致编译错误。public Date(){this(1900,1,1);}public Date(int year, int month, int day) {this();}/*无参构造器调用三个参数的构造器,而三个参数构造器有调用无参的构造器,形成构造器的递归调用编译报错:Error:(19, 12) java: 递归构造器调用*/在大多数情况下,我们使用 public 来修饰构造方法,但在特殊场景下(如实现单例模式)可能会使用 private 修饰构造方法。5.3 默认初始化在上文中提到的第二个问题:为什么局部变量在使用前必须初始化,而成员变量可以不初始化?要搞清楚这个过程,就需要知道 new 关键字背后所发生的一些事情:Date d = new Date(2021,6,9);在程序员看来只是一句简单的语句,但 JVM 层面需要做好多事情。下面简单介绍下:检测对象对应的类是否被加载,如果没有则加载为对象分配内存空间并先默认初始化处理并执行类中的 init 方法初始化分配好的空间 (说明:多个线程同时申请资源,JVM 要保证分配给对象的空间内干净。)即:对象空间被申请好之后,对象中包含的成员已经设置好了初始值,比如:Java中的 成员变量 会由 JVM 自动赋予默认值(如int类型为0,boolean类型为false等),而局部变量则需要显式初始化才能使用。Java为不同的数据类型提供了默认值,具体如下所示:数据类型 默认值byte 0char ‘\u0000’short 0int 0long 0Lboolean falsefloat 0.0fdouble 0.0dreference null这些默认值确保了即使对象的成员变量没有显式初始化,也不会发生错误,成员变量会有一个初始的稳定状态。设置对象头信息(关于对象内存模型后面会介绍)调用构造方法,给对象中各个成员赋值5.4 就地初始化就地初始化是指在成员变量声明时直接为它们赋初值。这种方法在代码的简洁性上具有优势,可以避免每次创建对象时重复设置成员变量的值。代码示例:public class Date {    public int year = 1900;  // 就地初始化    public int month = 1;    // 就地初始化    public int day = 1;      // 就地初始化    public Date() {        // 构造方法在此处不需要再次初始化year、month、day,它们已经有默认值    }        public void printDate() {        System.out.println(year + "-" + month + "-" + day);    }    public static void main(String[] args) {        Date d = new Date();        d.printDate();  // 输出:1900-1-1    }}在这个例子中,year、month 和 day 的值在声明时就被初始化为1900、1和1,确保了每次创建对象时这些值已经存在。六、封装6.1 封装的概念封装是面向对象编程的三大特性之一,其核心思想是将数据和操作数据的方法结合在一起,并对外隐藏实现细节。例如,电脑作为一个复杂的设备,用户只需通过开关、键盘和鼠标等接口进行交互,而不必关心内部CPU、显卡等工作原理。简单来说就是套壳屏蔽细节。6.2 访问限定符访问修饰符说明public:可以理解为一个人的外部接口,能被外部访问。protected:主要是给继承使用,子类继承后就能访问到。default(不写修饰符时即为默认访问修饰符):对于同一包内的类可见,不同包则不可见。private:只能在当前类中访问。(这部分要介绍完继承后才能完全理解)【说明】protected 主要是给继承使用;default 只能给同一个包内使用;按照自己的理解去记忆。示例代码:public class Computer {    private String cpu;    private String brand;    private String memory;    private String screen;    public Computer(String brand, String cpu, String memory, String screen) {        this.brand = brand;        this.cpu = cpu;        this.memory = memory;        this.screen = screen;    }    public void boot() {        System.out.println("开机");    }    public void shutDown() {        System.out.println("关机");    }}public class TestComputer {    public static void main(String[] args) {        Computer c = new Computer("华为", "i9", "16G", "4K");        c.boot();        c.shutDown();    }}注意:一般情况下,成员变量通常设置为 private,成员方法设置为 public。6.3 封装扩展之包6.3.1 包的概念包(Package)用于对类进行分组管理,有助于解决类名冲突并提高代码的组织性。例如,将相似功能的类归为同一包。为了更好的管理类,把多个类收集在一起成为一组,称为软件包6.3.3导入包在Java中,如果我们需要使用不在默认 java.lang 包中的类或接口,就需要使用 import 关键字来导入。例如,我们想使用 java.util.Date 这个类,就可以这样做:import java.util.Date;public class TestImport {    public static void main(String[] args) {        Date d = new Date();        System.out.println(d);    }}这样就可以正常创建 Date 对象并使用。6.3.3全类名当不同包中存在同名的类时(如 java.util.Date 与 java.sql.Date 都叫 Date),可能会引发冲突。这时,可以使用 全类名(Fully Qualified Name)来指定使用哪个类:public class TestFullName {    public static void main(String[] args) {        // 使用全类名来区分两个Date类        java.util.Date d1 = new java.util.Date();        java.sql.Date d2 = new java.sql.Date(System.currentTimeMillis());                System.out.println(d1);        System.out.println(d2);    }}通过在创建对象时加上包名,就可以区分来自 java.util 和 java.sql 的 Date 类。6.3.4 静态导入从 Java 5 开始,支持使用 静态导入(import static)的方式将某个类中的 静态成员(常量或方法) 导入到当前类中,从而在调用时可以省略类名。示例代码:import static java.lang.Math.PI;import static java.lang.Math.random;public class TestStaticImport {    public static void main(String[] args) {        System.out.println(PI);      // 直接使用PI常量        System.out.println(random()); // 直接使用random()方法    }}如果不使用静态导入,则需要写成:System.out.println(Math.PI);System.out.println(Math.random());6.3.5 IDE工具中的包结构当我们使用 IntelliJ IDEA 或 Eclipse 等 IDE 工具时,会在项目结构中直观地看到包名与文件夹一一对应。包名一般使用 小写 的域名反写形式(如 com.example.project),在 IDE 中会对应层级文件夹结构。在同一个包下,可以放置多个类文件,便于组织与管理。在实际开发中,合理划分包结构能让项目更易于维护和理解。6.3.6 包的访问权限控制举例Computer类位于com.bit.demo1包中,TestComputer位于com.bit.demo2包中:package com.bit.demo1;public class Computer {    private String cpu;        // cpu    private String memory;     // 内存    public String screen;      // 屏幕    String brand;              // 品牌    public Computer(String brand, String cpu, String memory, String screen) {        this.brand = brand;        this.cpu = cpu;        this.memory = memory;        this.screen = screen;    }    public void PowerOff(){        System.out.println("关机~~~");    }    public void SurfInternet(){        System.out.println("上网~~~");    }}package com.bit.demo2;import com.bit.demo1.Computer;public class TestComputer {    public static void main(String[] args) {        Computer p = new Computer("HW", "i7", "8G", "13*14");        System.out.println(p.screen);   // 公有属性,可以被其他包访问        // System.out.println(p.cpu);    // 私有属性,不能被其他包访问        // System.out.println(p.brand);  // brand是default,不允许被其他包中的类访问    }}注意:如果去掉前面的Computer类中的public修饰符,代码也会编译失败。6.3.7 常见的包java.lang:系统常用基础类(String,Object),此包从JDK1.1后自动导入。java.lang.reflect:Java反射机制包;java.net:进行网络编程开发包;java.sql:进行数据库开发的包;javax.util:Java提供的工具程序包(集合类等)非常重要;javalio.io:编程程序包。注意事项: import 和 C++ 的 #include 差别很大. C++ 必须 #include 来引入其他文件内容, 但是 Java 不需要.import 只是为了写代码的时候更方便.区别如下:Java 的 import:仅在编译时为你提供类或接口的简写路径,使你可以直接使用类名而不用写出完整的包名。实际上,编译器会在编译过程中通过类路径(classpath)去查找相应的类文件,而不会把代码插入到当前文件中。C/C++ 的 #include:预处理器会在编译之前将头文件的内容直接拷贝进源代码文件中,这种方式实际上是将文件的内容“粘贴”到包含它的文件里。因此,Java 的 import 只是一种简化引用的机制,并不涉及代码的复制。七、总结与展望在本篇文章中,我们围绕对象的构造与初始化、封装、包的管理以及访问权限等内容展开了详细讲解,帮助大家深入理解Java面向对象编程的核心概念。7.1 总结对象的构造与初始化构造方法:通过构造方法,我们可以在创建对象时立即为其成员赋值,保证对象在使用前处于有效状态。构造器重载与this调用:支持多个构造方法以及构造器链,让对象初始化更加灵活和简洁。默认初始化与就地初始化:成员变量会自动获得默认值,同时也可以在声明时直接赋值,避免重复代码。封装核心思想:将数据与操作数据的方法绑定在一起,通过访问限定符(public、private、protected、default)隐藏内部实现细节。访问控制:合理使用访问修饰符保护数据安全,提供公开接口供外部使用,增强了程序的健壮性和安全性。包的管理与导入包的概念:通过将相关类归为一组,实现代码的模块化管理和命名空间隔离,避免类名冲突。import语句:在编译时提供类名简写路径,与C/C++的#include机制不同,import不进行代码复制,仅起到标识和简化引用的作用。7.2 总结深入static成员在后续的内容中,我们将更详细地探讨static关键字的使用,包括静态变量、静态方法及其在类中的意义,帮助大家更好地理解类级别的共享特性。代码块与初始化块将介绍类中的初始化块和静态代码块,讨论它们在对象创建过程中的执行顺序和作用,为进一步掌握对象生命周期奠定基础。内部类与匿名类内部类是一种特殊的类定义方式,它能够更紧密地绑定外部类的成员,将在未来篇章中深入剖析其用法和设计思想。面向对象的设计原则随着对类和对象理解的深入,我们也将探讨更多面向对象设计的原则和模式,帮助大家构建更健壮、可维护的Java应用程序。———————————————— 原文链接:https://blog.csdn.net/2301_79849925/article/details/146015106
  • [技术干货] 计算机基础(三):深入解析Java中的原码、反码、补码
    引言  在Java的世界里,我们每天都在与整数打交道:int age = 30;、long balance = 1000000L;。但你是否思考过,这些数字在计算机内部的真实形态?理解原码、反码、补码不仅是计算机科学的基础,更是深入Java底层、避免隐蔽bug的关键。本文将带你彻底掌握这些二进制表示法的奥秘及其在Java中的实际应用。一、 基础概念:三种编码的诞生背景计算机只能处理二进制(0和1)。如何表示有符号整数(正数、负数)?工程师们设计了三种方案:1、原码定义:最高位表示符号位(0=正数,1=负数),其余位表示数值的绝对值示例 (8位byte为例):+5原码:0000 0101 (符号位0,绝对值5)-5原码:1000 0101 (符号位1,绝对值5)优点:人类直观,理解容易致命缺陷:存在两种零:+0的原码为00000000 ,-0的原码为10000000( 以一个字节长表示 ),浪费表示范围,逻辑上冗余加减运算复杂:正数和负数相加,数值部分实际上是相减,符号取决于绝对值大者的符号,硬件不能直接相加符号位和数值位示例1:(+5) + (-5) = 0,程序直接运算❌原码的加法不支持符号修正(不能自动让结果的符号与数值正确对应)    0000 0101  (+5)  + 1000 0101  (-5)  -----------------    1000 1010  结果为 -10示例2:(-5) + (+3) = -2,程序直接运算❌    1000 0101  (-5)  + 0000 0011  (+3)  -----------------    1000 1000  结果为 -8示例3:(-5) + (-5) = -10,程序直接运算❌原码的加法不支持进位消除(多出的进位被丢弃,不影响结果)    1000 0101  (-5)  + 1000 0101  (-5)  -----------------  1 0000 1010  结果为 10(注意:最高位溢出了1位舍弃)2、反码定义:正数:反码 = 原码负数:反码 = 负数原码符号位不变,数值位按位取反,或者更简单点正数原码全部按位取反示例 (8位byte为例):+5反码:0000 0101 (同原码)--5反码:1111 1010 (-5的原码1000 0101按位取反,符号位不变或者+5原码全部按位取反)遗留问题:两种零依然存在:+0反码为0000 0000,-0反码1111 1111(+0的原码按位取反)循环进位:计算结果最高位有进位时,需要把进位“回加”到最低位(称为“末位加1”或“循环进位”),硬件实现仍不理想示例1:(+5) + (-5) = 0,程序直接运算✅反码的加法在数值上可以看作正确,但从表达角度来看,有点不完美    0000 0101  (+5)  + 1111 1010  (-5)  -----------------    1111 1111  结果是反码,符号位不变其他按位取反,原码就是1000 0000也就是-0(负零)示例2:(-5) + (+3) = -2,程序直接运行✅    1111 1010  (-5)  + 0000 0011  (+3)  -----------------    1111 1101  结果是反码,原码为1000 0010也就是-2示例3:(-5) + (-5) = -10,程序直接运行❌反码的加法需要循环进位✅    1111 1010  (-5)  + 1111 1010  (-5)  -----------------  1 1111 0100  结果是反码(注意:最高位溢出了1位舍弃),最高位进位:1(需循环加回最低位)     1111 0100  +         1  -----------------    1111 0101 这里还是反码,原码为1000 1010也就是-103、补码 🏆 - Java的选择定义:正数:补码 = 原码负数:补码 = 反码 + 1示例 (8位byte为例):+5补码:0000 0101-5补码:+5的原码按位取反获得反码1111 1010,再加1获得补码1111 1011核心优势 (完美解决前两者问题):唯一的零:+0补码为0000 0000,-0补码是+0的原码全部按位取反再加1得到还是0000 0000,溢出一位舍去减法变加法:A - B = A + (-B) 直接成立,无需额外判断符号位或处理循环进位。硬件只需一套加法电路示例1:(+5) + (-5) = 0,程序直接运算✅补码加法支持进位消除(多出的进位被丢弃,不影响结果)    0000 0101  (+5)  + 1111 1011  (-5)  -----------------  1 0000 0001  结果是补码,先减1获得反码0000 0000,也就是0示例2:(-5) + (+3) = -2,程序直接运算✅    1111 1011  (-5)  + 0000 0011  (+3)  -----------------    1111 1110  结果是补码,先减1获得反码1111 1101,符号位不变其他按位取反获取原码1000 0010,也就是-2示例3:(-5) + (-5) = -10,程序直接运算✅    1111 1011  (-5)  + 1111 1011  (-5)  -----------------  1 1111 0110  结果为是补码,先减1获得反码1111 0101,符号位不变其他按位取反获得原码1000 1010,也就是-10二、 Java的坚定选择:补码一统天下1、为什么Java整数表示使用补码?补码解决了原码和反码的固有问题(双零、复杂运算),简化了CPU硬件设计,提高了运算效率最大优势正数和负数的加法、减法可以统一处理(解决痛点:原码需要判断正负)补码中只有一个 0(解决痛点:原码和反码有+0和-0,也需要单独判断)补码支持进位消除(解决痛点:符号参加运行,多出的进位丢弃,不影响结果,反码需要循环进位)Java的所有整数类型(byte, short, int, long)均使用补码表示!这是现代计算机体系结构的标准2、为什么byte的范围是-128到127,而不是-127到127?8 位二进制的表示能力Byte 类型占用 8 位(1 字节)存储空间,共有2^8 = 256种可能的二进制组合在补码体系中,最高位为符号位(0 正 1 负),剩余 7 位表示数值补码表示法的规则正数和零:补码与原码相同,范围是 0000 0000(0)到 0111 1111(127),共 128 个值负数:补码 = 原码取反 + 1,范围是 1000 0001(-127)到 1111 1111(-1),占 127 个值关键点:1000 0000 被定义为 -128二进制(补码)    十进制值1000 0000    -1281000 0001    -1271000 0010    -126...    …1111 1110    -21111 1111    -10000 0000    00000 0001    10000 0010    2...    …0111 1110    1260111 1111    127为何不是 -127 到 127?若范围设为 -127 到 127(含 0),仅能表示 127 + 128 = 255 127 + 128 = 255127+128=255 个值,无法覆盖全部 256 种组合补码的连续性要求:将 1000 0000 分配给 -128 后:数值序列形成闭环:127(0111 1111)+1 溢出为 -128(1000 0000),实现连续循环若不这样设计,会浪费一个二进制组合(1000 0000),且破坏数值连续性,-127(1000 0001)-1正好是-128(1000 0000)编程语言中的实际表现Byte.MAX_VALUE = 127,Byte.MIN_VALUE = -128赋值超出范围(如 byte b = 128;)会触发编译错误,如127 + 1 = -128(因 0111 1111 + 1 = 1000 0000)三、 眼见为实:Java代码验证补码Integer.toBinaryString(int i) 方法会返回一个整数补码表示的字符串(省略前导零,负数显示完整的32位,int占4个字节)public class ComplementDemo {    public static void main(String[] args) {        int positive = 5;        int negative = -5;        // 打印正数5的二进制(补码,省略前导零)        System.out.println(Integer.toBinaryString(positive)); // 输出: 101        // 打印负数-5的二进制(32位完整补码)        System.out.println(Integer.toBinaryString(negative)); // 输出: 1111 1111 1111 1111 1111 1111 1111 1011    }}解读负数输出:11111111111111111111111111111011 就是 -5 的 32 位补码它是由 +5 (00000000_00000000_00000000_00000101) 按位取反 (11111111_11111111_11111111_11111010)再加 1 得到的 (11111111_11111111_11111111_11111011)总结原码:直观但有双零,运算复杂(历史概念)反码:试图改进运算,仍有双零和循环进位问题(历史概念)补码 (Java的选择):统一零表示,完美支持 A - B = A + (-B),硬件实现高效简单,是现代计算机整数表示的标准Java实践:byte, short, int, long 均用补码。Integer.toBinaryString() 可查看补码形式————————————————原文链接:https://blog.csdn.net/qq_35512802/article/details/148684625
  • [技术干货] JVM内存泄漏问题怎么排查
    在排查 JVM 内存泄漏问题时,需要综合利用监控工具、日志分析、堆转储(Heap Dump)分析和代码审查,逐步缩小问题范围,定位泄漏源。下面介绍一种系统化的排查流程及常用方法:1. 监控与日志分析监控内存使用趋势利用 Prometheus、Grafana、JConsole、VisualVM 等工具,监控堆内存、非堆内存、GC 次数和停顿时间。如果发现堆内存使用率持续上升,或者 GC 次数频繁增加(尤其是 Full GC 频率增加),可能存在内存泄漏。开启 GC 日志配置 JVM 参数(例如:-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log)记录垃圾回收日志。分析 GC 日志,观察垃圾回收前后堆内存的变化,是否存在“GC overhead limit exceeded”异常等。2. Heap Dump 分析采集 Heap Dump当发现内存使用异常时,可以使用 jmap -dump 或者通过监控工具(如 JVisualVM、Java Mission Control)生成堆转储文件(.hprof 文件)。可以在 OOM(OutOfMemoryError)发生时自动生成 Heap Dump(通过 -XX:+HeapDumpOnOutOfMemoryError)。使用内存分析工具利用 Eclipse Memory Analyzer Tool(MAT)、VisualVM 的插件或 JProfiler 分析 Heap Dump。主要分析点:找出占用内存最多的对象及其数量(疑似泄漏点)。查看对象的引用链(Reference Chain),定位哪些引用没有及时释放,比如长生命周期的静态变量、集合(List、Map)等未清理的容器。分析类加载器问题,排查是否存在因为类未卸载导致的内存泄漏(尤其在动态部署的场景)。3. 代码审查与排查工具代码审查检查是否存在未关闭的资源(如数据库连接、IO 流)、缓存未清理、静态集合持续累积数据等常见内存泄漏原因。注意循环引用、事件监听器未注销、定时任务中使用匿名内部类等情况。使用专业工具使用 JProfiler、YourKit 等商业分析工具,进行实时内存监控和泄漏检测,观察对象分配和垃圾回收情况。断点调试与日志记录对可疑模块加入内存监控日志、统计特定对象的创建与释放,辅助排查内存泄漏的细节。4. 调优与验证调整 GC 参数根据监控结果和 Heap Dump 分析结果,调优垃圾回收参数(如堆大小、年轻代比例、GC 算法),改善内存回收效果,并观察是否能缓解内存增长问题。持续集成和测试编写压力测试和长时间运行测试,验证问题修复情况。采用内存泄漏检测工具(如 Java 的 LeakCanary 或 Apache JMeter 配合内存监控)持续监控内存使用。总结排查 JVM 内存泄漏问题的基本步骤是:监控与日志分析:观察内存使用趋势和 GC 日志,确认异常现象。Heap Dump 分析:采集堆转储,利用 MAT 或其他工具查找内存占用最多的对象和引用链。代码审查与调试:检查资源释放、缓存管理和事件监听等代码,结合专业工具进行实时监控。调优与验证:调整 JVM 参数,优化代码后通过测试验证内存泄漏是否得到解决。通过这种系统化的排查方法,可以较快定位内存泄漏的根本原因,并在此基础上进行相应的优化和修复。————————————————原文链接:https://blog.csdn.net/longgelaile/article/details/145513376
  • [技术干货] 面向对象进阶 | 深入探究 Java 静态成员与继承体系
    一、static(静态)static表示静态,是Java中的一个修饰符,可以修饰成员方法,成员变量。1.static 静态变量被static修饰的成员变量,叫做静态变量。特点:被该类的所有对象共享不属于对象,属于类随着类的加载而加载,优于对象存在调用方式:类名调用(推荐)对象名调用代码展示需求:写一个JavaBean类来描述这个班级的学生属性:姓名、年龄、性别行为:学习JAVA Bean 类package staticdemo;public class Student {    //属性:姓名,年龄,性别    //新增:老师的姓名    private String name;    private int age;    private String gender;    public  static  String teacherName;    public Student() {    }    public Student(String name, int age, String gender) {        this.name = name;        this.age = age;        this.gender = gender;    }    /**     * 获取     * @return name     */    public String getName() {        return name;    }    /**     * 设置     * @param name     */    public void setName(String name) {        this.name = name;    }    /**     * 获取     * @return age     */    public int getAge() {        return age;    }    /**     * 设置     * @param age     */    public void setAge(int age) {        this.age = age;    }    /**     * 获取     * @return gender     */    public String getGender() {        return gender;    }    /**     * 设置     * @param gender     */    public void setGender(String gender) {        this.gender = gender;    }    //行为    public void study(){        System.out.println(name+"正在学习");    }    public void show(){        System.out.println(name+","+age+","+gender+","+teacherName);    }}测试类package staticdemo;public class StudentTest {    public static void main(String[] args) {        //1.创建第一个学生对象        Student s1 = new Student();        s1.setName("张三");        s1.setAge(20);        s1.setGender("男");        //公共类,但是s2没有创建对象,所以无法访问teacherName,为null        //public String teacherName;        //于是我们想了个方法,用static修饰teacherName,但是这样就变成了静态属性,所有对象共享        //public  static  String teacherName;        s1.teacherName = "王老师";        //还可以用类名.属性名来访问        //Student.teacherName = "王老师";        s1.study();        s1.show();        //2.创建第二个学生对象        Student s2 = new Student();        s2.setName("李四");        s2.setAge(21);        s2.setGender("女");        s2.study();        s2.show();    }}内存图栈内存方法调用时会在栈内存中创建栈帧。这里main方法首先入栈 ,在main方法执行过程中:执行 Student.teacherName = "阿玮老师"; ,这一步只是对静态变量赋值,在栈内存中记录这个操作。执行 Student s1 = new Student(); 时,在栈内存为引用变量 s1 分配空间,存放指向堆内存中 Student 对象的地址(假设为 0x0011 ) 。执行 s1.name = "张三"; 和 s1.age = 23; ,是通过 s1 引用操作堆内存中对象的实例变量。执行 s1.show(); 时,show 方法的栈帧入栈,在栈帧中记录方法内的局部变量(这里无额外局部变量)以及要操作的对象属性(通过 s1 找到堆内存对象属性)。执行 Student s2 = new Student(); ,在栈内存为引用变量 s2 分配空间,存放指向堆内存中另一个 Student 对象的地址(假设为 0x0022 ) 。执行 s2.show(); 时,show 方法栈帧再次入栈,通过 s2 引用操作其对应的堆内存对象属性。堆内存当执行new Student()时,在堆内存创建Student对象实例。第一个 Student 对象(对应 s1 ),在堆内存中分配空间存储实例变量 name 值为 “张三” ,age 值为 23 。第二个 Student 对象(对应 s2 ),在堆内存中分配空间存储实例变量 name 初始值 null (字符串默认初始值) ,age 初始值 0 (整数默认初始值) 。静态变量 teacherName 存储在堆内存的静态存储位置(静态区),值为 “阿玮老师” ,所有 Student 类的对象共享这个静态变量。注意:静态变量随类的出现而出现,优于变量。2.static 静态方法被static修饰的成员方法,叫做静态方法。特点:多用在测试类和工具类中Javabean类中很少会用调用方式:类名调用(推荐)对象名调用工具类:帮助我们做一些事情的,但是不描述任何事物的类Javabean类:用来描述一类事物的类。比如:Student、Teather、Dog等测试类:用来检查其他类是否书写正确,带有main方法的类,是程序的入口遵守的规范:类名见名知意私有化构造方法方法定义为静态的练习:第一题:需求:在实际开发中,经常会遇到一些数组使用的工具类请按照如下要求编写一个数组的工具类:ArrayUtil工具类:package sta02;public class ArrayUtil {    //私有构造方法,防止外部实例化    private ArrayUtil() {}    public static String printArray(int[] arr) {        StringBuilder sb = new StringBuilder();        sb.append( "[");        for (int i = 0; i < arr.length; i++) {            sb.append(arr[i]);            if (i < arr.length - 1) {                sb.append(", ");            }else {                sb.append("]");            }        }        return sb.toString();    }    public static double getArray(double[] arr) {        double sum = 0;        for (int i = 0; i < arr.length; i++) {            sum += arr[i];  //累加数组元素        }        return sum/arr.length;    }}测试类package sta02;public class Testdemo {    public static void main(String[] args) {        //测试printArray方法        int[] arr = {1, 2, 3, 4, 5};        String result = ArrayUtil.printArray(arr);        System.out.println(result);        //测试getArray方法        double[] arr2 = {1.0, 2.0, 3.0, 4.0, 5.0};        double average = ArrayUtil.getArray(arr2);        System.out.println(average);    }}第二题:需求:定义一个集合,用于存储3个学生对象学生类的属性:name、age、gender定义一个工具类,用于获取集合中最大学生的年龄JavaBean类:package sat03;public class Student {    private String name;    private int age;    private String gender;    public Student() {    }    public Student(String name, int age, String gender) {        this.name = name;        this.age = age;        this.gender = gender;    }    /**     * 获取     * @return name     */    public String getName() {        return name;    }    /**     * 设置     * @param name     */    public void setName(String name) {        this.name = name;    }    /**     * 获取     * @return age     */    public int getAge() {        return age;    }    /**     * 设置     * @param age     */    public void setAge(int age) {        this.age = age;    }    /**     * 获取     * @return gender     */    public String getGender() {        return gender;    }    /**     * 设置     * @param gender     */    public void setGender(String gender) {        this.gender = gender;    }}方法类:package sat03;import java.util.ArrayList;public class StudentUtil {    //私有构造方法,防止外部实例化    private StudentUtil() {}    //静态方法    public static int getMaxScore(ArrayList<Student> list ) {        //1.定义一个参照物        int maxAge = list.get(0).getAge();        //2.遍历集合        for (int i = 1; i < list.size(); i++) {            if (list.get(i).getAge() > maxAge) {                maxAge = list.get(i).getAge();            }        }        return maxAge;    }}测试类:package sat03;import java.util.ArrayList;public class testmax {    public static void main(String[] args) {        //1.创建一个集合来存储        ArrayList<Student> list = new ArrayList<Student>();        //2.创建3个学生对象        Student s1 = new Student("Zhangsan", 20, "男");        Student s2 = new Student("lisi", 23, "男");        Student s3 = new Student("wangwu", 25, "女");        //3.将学生对象添加到集合中        list.add(s1);        list.add(s2);        list.add(s3);        //4.调用方法        int max=StudentUtil.getMaxScore(list);        System.out.println(max);    }}3.static注意事项静态方法只能访问静态变量和静态方法非静态方法可以访问静态变量或者静态方法,也可以访问非静态的成员变量和非静态的成员方法静态方法中是没有this关键字的总结:静态方法中,只能访问静态非静态方法可以访问所有静态方法中没有this关键字4.重新认识main方法public class HelloWorld{    public static void main (String[] args){        System.out.println("HelloWorld");    }}public : 被JVM调用,访问权限足够大static :被JVM调用,不用创建对象,直接类名访问​ 因为main方法是静态的,所以测试类中其他方法也是需要是静态的void : 被JVM调用,不需要给JVM返回值main : 一个通用的名称,虽然不是关键字,但是被JVM识别String[] args :以前用于接受键盘录入数据的,现在没用二、继承面向对象三大特征:封装、继承、多态封装:对象代表什么,就得封装对应的数据,并提供数据对应的行为。我们发现在Student类与Teacher类中有重复的元素,于是为了使程序更加便捷便出现了”继承“1.继承概述java中提供一个关键字extends,用这个关键字,我们可以让一个类和另一个类建立起继承关系public class Student extends Person{}1Student称为子类(派生类),Person称为父类(基类或超类)优点:可以把多个子类中重复的代码抽取到父类中,提高代码的复用性子类可以在父类的基础上,增加其他的功能,使子类更强大什么时候用继承?当类与类之间,存在相同(共性)的内容,并满足子类是父类中的一种,就可以考虑使用继承,来优化代码2.继承的特点java中只支持单继承,不支持多继承,但支持多层继承单继承:一个子类只能继承一个直接父类不支持多继承:子类不能同时继承多个父类多层继承:子类A继承父类B,父类B可以继承父类CC是A的间接父类每一个类都直接或者间接的继承于Object练习:核心点:共性内容抽取,子类是父类中的一种写代码是从父类开始写,最后写子类JAVABean类package st5;public class Animal {    public void eat() {        System.out.println("我会吃饭");    }    public void water (){        System.out.println("我会喝水");    }}package st5;public class Cat extends Animal {    public void mice() {        System.out.println("我会抓老鼠");    }}package st5;public class Dog extends Animal {    public void lookhome() {        System.out.println("我会看家");    }}package st5;public class Ragdoll extends Cat{}package st5;public class Lihua extends Cat{}package st5;public class Husky extends Dog{    public void breakhome() {        System.out.println("我会拆家");    }}package st5;public class Teddy extends Dog{    public void Ceng(){        System.out.println("我喜欢蹭一蹭");    }}测试类:package st5;public class Test {    public static void main(String[] args) {        //创建对象并调用方法        //创建布偶猫的对象        Ragdoll rd = new Ragdoll();        System.out.println("我是布偶猫");        rd.mice();        rd.water();        rd.eat();        System.out.println("-------------------");        //创建狸花猫的对象        Lihua lh = new Lihua();        System.out.println("我是狸花猫");        lh.mice();        lh.water();        lh.eat();        System.out.println("-------------------");        //创建泰迪的对象        Teddy td = new Teddy();        System.out.println("我是泰迪");        td.lookhome();        td.water();        td.eat();        td.Ceng();        System.out.println("-------------------");        //创建哈士奇的对象        Husky hs = new Husky();        System.out.println("我是哈士奇");        hs.lookhome();        hs.water();        hs.eat();        hs.breakhome();    }}试运行:注意:子类只能访问父类中非私有的成员3.子类到底能继承父类中的哪些内容构造方法    非私有 不能    private 不能成员变量    非私有 能    private 能 但不能直接用成员方法    虚方法表 能    否则 不能虚方法表:就是经常要用的方法,什么叫虚方法表呢?非private 非static非final4.继承中访问特点继承中:成员变量的访问特点public class Fu{    String name = "Fu";}public class Zi extends Fu{    String name = "Zi";    public void ziShow(){        String name = "ziShow";        System.out.println(name);    }}//就近原则:谁离我近,我就用谁//完整版就近原则:先在局部位置找,本类成员位置找,父类成员位置找,逐级往上//run:ziShow如果出现了重名的成员变量怎么找:System.out.println(name);//从局部位置开始往上找System.out.println(this.name);//从本类成员位置开始往上找System.out.println(super.name);//从父类成员位置开始往上找public class Test{    public static void main (String [] args){        Zi z = new Zi();        z.ziShow();    }}public class Fu{    String name = "Fu";}public class Zi extends Fu{    String name = "Zi";    public void ziShow(){        String name = "ziShow";        System.out.println(name);//ziShow        System.out.println(this.name);//Zi        System.out.println(super.name);//Fu    }}继承中:成员方法的访问特点直接调用满足就近原则:谁离我近,我就用谁super调用,直接访问父类package jicehng;public class test {    public static void main(String[] args) {        //创建一个对象        Student s = new Student();        s.lunch();        /*        吃面条        咖啡        吃米饭        喝水         */    }}class Person {    public void eat(){        System.out.println("吃米饭");    }    public void water(){        System.out.println("喝水");    }}class Student extends Person {    public void lunch(){        this.eat();//就近读取子类吃面条        this.water();//就近读取子类咖啡        super.eat();//调用父类吃米饭        super.water();//调用父类喝水    }    public void eat(){        System.out.println("吃面条");    }    public void water(){        System.out.println("咖啡");    }}方法的重写:当父类的方法不能满足子类现在的需求时,需要进行方法重写书写格式:在继承体系中 ,子类出现了和父类中一模一样的方法声明,我们就称子类这个方法是重写的方法@Override重写体系@Override是放在重写后的方法上,校验子类重写时语法是否正确加上注解后如果有红色波浪线,表示语法错误建议重写方法都加@Override注解,代码安全,优雅!方法重写注意事项和要求:重写方法的名称、形参列表必须于父类中的一致子类重写父类方法时,访问权限子类必须大于等于父类(暂时了解:空着不写<protected<public)子类重写父类方法时,返回值类型必须小于等于父类建议:重写的方法尽量和父类保持一致只有被添加到虚方法表中的方法才能被重写JAVABean类:package jice;public class Dog {    public void eat() {        System.out.println("Dog is eating.");    }    public void drink() {        System.out.println("Dog is drinking.");    }    public void lookhome() {        System.out.println("Dog is lookhome.");    }}package jice;public class hashiqi extends Dog {    public void breakhome() {        System.out.println("hashiqi is breakhome.");    }}package jice;public class shapi extends Dog{    @Override    public void eat() {        super.eat();// 调用父类的eat方法        System.out.println("shapi is eating gouliang.");    }}package jice;public class chinesedog extends Dog{    @Override    public void eat() {        super.eat();// 调用父类的eat方法        System.out.println("Chinesedog is eating chinesefood.");    }}测试类package jice;public class test {    public static void main(String[] args) {        hashiqi hashiqi = new hashiqi();        hashiqi.eat();        hashiqi.drink();        hashiqi.lookhome();        shapi shapi = new shapi();        shapi.eat();        shapi.drink();        shapi.lookhome();        chinesedog chinesedog = new chinesedog();        chinesedog.eat();        chinesedog.drink();    }}继承中:构造方法的访问特点父类中的构造方法不会被子类继承子类中所有的构造方法默认先访问父类中的无参构造,再执行自己为什么?子类在初始化的时候,有可能会使用到父类中的数据,如果父类没有完成初始化,子类将无法使用父类的数据。子类初始化之前,一定要调用父类构造方法先完成父类数据空间的初始化怎么调用父类构造方法的?子类构造方法的第一行语句默认都是:super(),不写也存在,且必须在第一行如果想调用父类有参构造,必须手动写super进行调用this、super使用总结this:理解为一个变量,表示当前发给发调用者的地址值;super:代表父类存储空间关键字    访问成员变量    访问成员方法    访问构造方法this    this.成员变量 访问本类成员变量    this.成员方法(…) 访问本类成员方法    this(…) 访问本类构造方法super    super.成员变量 访问父类成员变量    super.成员方法(…) 访问父类成员方法    super(…) 访问父类构造方法 chinesedog chinesedog = new chinesedog();    chinesedog.eat();    chinesedog.drink();}}### 继承中:构造方法的访问特点- 父类中的构造方法不会被子类继承- 子类中所有的构造方法默认先访问父类中的无参构造,再执行自己  为什么?  - 子类在初始化的时候,有可能会使用到父类中的数据,如果父类没有完成初始化,子类将无法使用父类的数据。  - 子类初始化之前,一定要调用父类构造方法先完成父类数据空间的初始化  怎么调用父类构造方法的?  - 子类构造方法的第一行语句默认都是:`super()`,不写也存在,且必须在第一行  - 如果想调用父类有参构造,必须手动写`super`进行调用### this、super使用总结this:理解为一个变量,表示当前发给发调用者的地址值;super:代表父类存储空间| 关键字 | 访问成员变量                     | 访问成员方法                           | 访问构造方法                 || ------ | -------------------------------- | -------------------------------------- | ---------------------------- || this   | this.成员变量  访问本类成员变量  | this.成员方法(...) 访问本类成员方法  | this(...)  访问本类构造方法  || super  | super.成员变量  访问父类成员变量 | super.成员方法(...) 访问父类成员方法 | super(...)  访问父类构造方法 |————————————————原文链接:https://blog.csdn.net/2401_87533975/article/details/148388640
  • [技术干货] 【踩坑系列】使用httpclient调用第三方接口返回javax.net.ssl.SSLHandshakeException异常
    1. 踩坑经历最近做了个需求,需要调用第三方接口获取数据,在联调时一直失败,代码抛出javax.net.ssl.SSLHandshakeException异常,具体错误信息如下所示:javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target2.原因分析因为调用第三方接口的代码是复用项目中原有的工具类(基于httpclient封装),所以在确认完传参没问题后,第一时间排除了编码问题。然后开始怀疑第三方提供的接口地址(因为竟然是IP+端口访问),在和第三方确认没有域名访问后,在浏览器里输入第三方的接口地址,发现证书有问题:又使用Postman调用第三方接口,也是失败,提示自签名证书:通过以上分析,可以发现出现该问题的根本原因是Java客户端不信任目标服务器的SSL证书,比如这个第三方使用的自签名证书。3.解决方案解决方案一般有2种,第1种方案是将服务器证书导入Java信任库,第2种方案是绕过SSL验证,这里采用第2种方案。首先,新建HttpClient工具类:import org.apache.http.conn.ssl.NoopHostnameVerifier;import org.apache.http.conn.ssl.SSLConnectionSocketFactory;import org.apache.http.impl.client.CloseableHttpClient;import org.apache.http.impl.client.HttpClients;import javax.net.ssl.SSLContext;import javax.net.ssl.TrustManager;import javax.net.ssl.X509TrustManager;import java.security.KeyManagementException;import java.security.NoSuchAlgorithmException;import java.security.cert.X509Certificate;public class HttpClientUtils {    public static CloseableHttpClient createIgnoreCertClient() throws NoSuchAlgorithmException, KeyManagementException {        SSLContext sslContext = SSLContext.getInstance("SSL");        sslContext.init(null, new TrustManager[]{new X509TrustManager() {            @Override            public X509Certificate[] getAcceptedIssuers() {                return null;            }            @Override            public void checkClientTrusted(X509Certificate[] certs, String authType) {            }            @Override            public void checkServerTrusted(X509Certificate[] certs, String authType) {            }        }}, new java.security.SecureRandom());        SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);        return HttpClients.custom().setSSLSocketFactory(sslConnectionSocketFactory).build();    }}然后将原来声明httpClient的代码改为如下所示:CloseableHttpClient httpClient = HttpClientUtils.createIgnoreCertClient();注意事项:确保项目中引入了httpclient依赖:<dependency>    <groupId>org.apache.httpcomponents</groupId>    <artifactId>httpclient</artifactId>    <version>4.5.13</version></dependency>转载自https://www.cnblogs.com/zwwhnly/p/18795523
  • [技术干货] 【深度思考】自定义日期格式,为什么@JSONField生效,@JsonFormat不生效?
    1. 前言最近在自测接口时,发现一个问题:字段类型定义的是Date,但接口返回值里却是时间戳(1744959978674),而不是预期的2025-04-18 15:06:18。private Date useTime;{ "code": "200", "message": "", "result": [ { "id": 93817601, "useTime": 1744959978674 } ]}这种返回值,无法快速的知道是哪个时间,如果想知道时间对不对,还得找一个时间戳转换工具做下转换才能确定,非常不方便。因此想让接口直接返回预期的2025-04-18 15:06:18格式。刚开始,在字段上添加了@JsonFormat注解,发现没生效,返回的还是时间戳:import com.fasterxml.jackson.annotation.JsonFormat;@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss")private Date useTime;然后,改成了@JSONField注解,发现生效了,达到了预期的结果:import com.alibaba.fastjson.annotation.JSONField;@JSONField(format = "yyyy-MM-dd HH:mm:ss")private Date useTime;{ "code": "200", "message": "", "result": [ { "id": 93817601, "useTime": "2025-04-18 15:06:18" } ]}那么问题来了,为啥@JSONField生效,@JsonFormat不生效?2. 原因分析默认情况下,Spring Boot使用的JSON消息转换器是Jackson的MappingJackson2HttpMessageConverter,核心依赖为:<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.8.11.3</version></dependency>现在使用Jackson的@JsonFormat注解不生效,说明Spring Boot没有使用默认的MappingJackson2HttpMessageConverter。使用fastjson的@JSONField注解生效了,说明Spring Boot使用的是fastjson下的JSON消息转换器,也就是FastJsonHttpMessageConverter,依赖为:<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.83</version></dependency>那么怎么找到代码在哪配置的呢?第一步,先在项目中全局搜索FastJsonHttpMessageConverter(Windows快捷键:Ctrl+Shift+F),不过大概率是搜索不到,因为公司里的项目一般都继承自公司公共的xxx-spring-boot-starter。第二步,连按2次Shift键搜索FastJsonHttpMessageConverter,然后查找该类的引用或者子类(子类很可能是公司底层框架中写的)。然后,很可能会找到类似下面的代码:@Configurationpublic class FastJsonMessageConverterConfig { @Bean public HttpMessageConverters customConverters() { FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter(); return new HttpMessageConverters(new HttpMessageConverter[]{fastJsonHttpMessageConverter}); }}以上代码显式注册了一个FastJsonHttpMessageConverter,并通过HttpMessageConverters覆盖了默认的HTTP 消息转换器(Jackson的MappingJackson2HttpMessageConverter),所以Spring MVC将只使用fastjson处理JSON序列化/反序列化。这也是@JSONField生效,@JsonFormat不生效的根本原因。3. 默认行为及全局配置fastjson 1.2.36及以上版本,默认将日期序列化为时间戳(如1744959978674),如果要默认将日期序列化为yyyy-MM-dd HH:mm:ss(如2025-04-18 15:06:18),需要启用WriteDateUseDateFormat特性:@Configurationpublic class FastJsonMessageConverterConfig { @Bean public HttpMessageConverters customConverters() { FastJsonConfig fastJsonConfig = new FastJsonConfig(); // 启用日期格式化特性(禁用时间戳) fastJsonConfig.setSerializerFeatures(SerializerFeature.WriteDateUseDateFormat); // 设置日期格式(不指定时,默认为yyyy-MM-dd HH:mm:ss,但即使与默认值一致,也建议明确指定) fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss"); FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter(); fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig); return new HttpMessageConverters(new HttpMessageConverter[]{fastJsonHttpMessageConverter}); }}如果某个日期字段有特殊序列化要求,可以使用@JSONField注解灵活配置(该注解会覆盖全局配置):import com.alibaba.fastjson.annotation.JSONField;@JSONField(format = "yyyy-MM-dd")private Date anotherUseTime;注意事项:修改全局配置需慎重,如果一个老项目,原来日期类型返回的都是时间戳,突然全部改为返回字符串,可能会造成调用方报错。转载自https://www.cnblogs.com/zwwhnly/p/18838062
  • [技术干货] 【踩坑系列】使用Comparator.comparing对中文字符串排序结果不对
    1. 踩坑经历假设有这样一个业务场景,需要对各个城市的订单量排序,排序规则为:先根据订单量倒序排列,再根据城市名称正序排列。示例代码:import lombok.Getter;import lombok.Setter;import lombok.ToString;@Getter@Setter@ToStringpublic class OrderStatisticsInfo { private String cityName; private Integer orderCount; public OrderStatisticsInfo(String cityName, Integer orderCount) { this.cityName = cityName; this.orderCount = orderCount; }}public static void main(String[] args) { List<OrderStatisticsInfo> orderStatisticsInfoList = Arrays.asList( new OrderStatisticsInfo("上海", 1000), new OrderStatisticsInfo("北京", 1000), new OrderStatisticsInfo("成都", 700), new OrderStatisticsInfo("常州", 700), new OrderStatisticsInfo("广州", 900), new OrderStatisticsInfo("深圳", 800) ); orderStatisticsInfoList.sort(Comparator.comparing(OrderStatisticsInfo::getOrderCount, Comparator.reverseOrder()) .thenComparing(OrderStatisticsInfo::getCityName)); orderStatisticsInfoList.forEach(System.out::println);}预期结果:北京 1000上海 1000广州 900深圳 800常州 700成都 700实际结果:OrderStatisticsInfo(cityName=上海, orderCount=1000)OrderStatisticsInfo(cityName=北京, orderCount=1000)OrderStatisticsInfo(cityName=广州, orderCount=900)OrderStatisticsInfo(cityName=深圳, orderCount=800)OrderStatisticsInfo(cityName=常州, orderCount=700)OrderStatisticsInfo(cityName=成都, orderCount=700)从以上结果可以看出,根据订单量倒序排列没啥问题,但根据城市名称正序排列不符合预期:上海竟然排到了北京的前面,但常州与成都的顺序又是对的。2. 原因分析Comparator.comparing对字符串类型进行排序时,默认使用的是字符串的自然排序,即String的compareTo方法,该方法是基于Unicode编码值进行比较的,未考虑语言特定的字符顺序(如中文拼音)。先看下String的compareTo方法的源码:public int compareTo(String anotherString) { int len1 = value.length; int len2 = anotherString.value.length; int lim = Math.min(len1, len2); char v1[] = value; char v2[] = anotherString.value; int k = 0; while (k < lim) { char c1 = v1[k]; char c2 = v2[k]; if (c1 != c2) { return c1 - c2; } k++; } return len1 - len2;}以上海与北京的比较为例,先比较第一个字符,也就是字符上和字符北,字符上对应的Unicode编码值是19978,因此c1 = 19978,字符北对应的Unicode编码值是21271,因此c2 = 21271,因为c1 != c2,所以返回值为-1293,也就是说上海小于北京(要排在北京的前面),不符合预期。以常州与成都的比较为例,先比较第一个字符,也就是字符常和字符成,字符常对应的Unicode编码值是24120,因此c1 = 24120,字符成对应的Unicode编码值是25104,因此c2 = 25104,因为c1 != c2,所以返回值为-984,也就是说常州小于成都(要排在成都的前面),符合预期。可以通过Character.codePointAt方法获取字符的Unicode编码值:// 输出:19978System.out.println(Character.codePointAt("上海", 0));// 输出:21271System.out.println(Character.codePointAt("北京", 0));// 输出:24120System.out.println(Character.codePointAt("常州", 0));// 输出:25104System.out.println(Character.codePointAt("成都", 0));3. 解决方案Java提供了本地化的排序规则,可以按特定语言规则排序(如中文拼音),代码如下所示:orderStatisticsInfoList.sort(Comparator.comparing(OrderStatisticsInfo::getOrderCount, Comparator.reverseOrder()) .thenComparing(OrderStatisticsInfo::getCityName, Collator.getInstance(Locale.CHINA)));orderStatisticsInfoList.forEach(System.out::println);此时的输出结果为:OrderStatisticsInfo(cityName=北京, orderCount=1000)OrderStatisticsInfo(cityName=上海, orderCount=1000)OrderStatisticsInfo(cityName=广州, orderCount=900)OrderStatisticsInfo(cityName=深圳, orderCount=800)OrderStatisticsInfo(cityName=常州, orderCount=700)OrderStatisticsInfo(cityName=成都, orderCount=700)可以看到,北京排到了上海的前面,符合预期。上述代码指定了Collator.getInstance(Locale.CHINA),在排序比较时不再执行String的compareTo方法,而是执行Collator的compare方法,实际上是RuleBasedCollator的compare方法。可以执行以下代码单独看下上海与北京的比较结果:Collator collator = Collator.getInstance(Locale.CHINA);// 输出:1,代表上海大于北京,也就是要排在北京的后面System.out.println(collator.compare("上海", "北京"));转载自https://www.cnblogs.com/zwwhnly/p/18846441
  • [技术干货] 聊聊@Autowired注解的Field injection is not recommended提示问题
    1. 前言在我接触过的大部分Java项目中,经常看到使用@Autowired注解进行字段注入:import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Servicepublic class OrderService { @Autowired private PaymentService paymentService; @Autowired private InventoryService inventoryService;}在IDEA中,以上代码@Autowired注解下会显示波浪线,鼠标悬停后提示:Field injection is not recommended,翻译过来就是不建议使用字段注入。关于该提示问题,有直接修改IDEA设置关闭该提示的,有替换为使用@Resource注解的,但这都不是该问题的本质。该问题的本质是Spring官方推荐使用构造器注入,IDEA作为一款智能化的IDE,针对该项进行了检测并给以提示。所以该提示背后的本质问题是:为什么Spring官方推荐构造器注入而不是字段注入?2. 推荐构造器注入的理由相比字段注入,构造器注入有以下几个优点:支持不可变性依赖明确单元测试友好循环依赖检测前置,提前暴露问题2.1 支持不可变性构造器注入允许将依赖字段声明为final,确保对象一旦创建,其依赖关系不再被修改。字段注入无法使用final,依赖可能在对象生命周期中被意外修改,破坏状态一致性。构造器注入示例:import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Servicepublic class OrderService { private final PaymentService paymentService; private final InventoryService inventoryService; @Autowired public OrderService(PaymentService paymentService, InventoryService inventoryService) { this.paymentService = paymentService; this.inventoryService = inventoryService; }}说明:如果Spring版本是4.3或者更高版本且只有一个构造器,构造器上的@Autowired注解可以省略。2.2 依赖明确构造器注入通过在类的构造函数中显式声明依赖,并且强制要求在创建对象时必须提供所有必须的依赖项,通过构造函数参数,使用者对该类的依赖一目了然。字段注入通过在类的字段上直接使用@Autowired注解注入依赖,依赖关系隐藏在类的内部,使用者无法直接看到该类的依赖。2.3 单元测试友好构造器注入允许直接通过new创建对象,无需依赖Spring容器或反射,降低了测试复杂度。字段注入需要依赖Spring容器或反射,增加了测试复杂度。2.4 循环依赖检测前置,提前暴露问题构造器注入在应用启动时直接暴露循环依赖,强制开发者通过设计解决问题。字段注入在应用启动时不会暴露循环依赖,直到实际调用时才可能暴露问题,增加调试难度。示例:假设项目中有以下两个Service存在循环依赖:import org.springframework.stereotype.Service;@Servicepublic class OrderService { private final PaymentService paymentService; public OrderService(PaymentService paymentService) { this.paymentService = paymentService; }}import org.springframework.stereotype.Service;@Servicepublic class PaymentService { private final OrderService orderService; public PaymentService(OrderService orderService) { this.orderService = orderService; }}此时启动项目会报错,抛出org.springframework.beans.factory.BeanCurrentlyInCreationException异常,大致的异常信息如下所示:Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'orderService': Requested bean is currently in creation: Is there an unresolvable circular reference?将以上两个Service修改为字段注入:import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Servicepublic class OrderService { @Autowired private PaymentService paymentService;}import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Servicepublic class PaymentService { @Autowired private OrderService orderService;}此时启动项目不会报错,可以启动成功。3. @RequiredArgsConstructor注解的使用及原理为了避免样板化代码或者为了简化代码,有的项目中可能会使用@RequiredArgsConstructor注解来代替显式的构造方法:import lombok.RequiredArgsConstructor;import org.springframework.stereotype.Service;@RequiredArgsConstructor@Servicepublic class OrderService { private final PaymentService paymentService; private final InventoryService inventoryService;}接下来简单讲解下@RequiredArgsConstructor注解的原理。@RequiredArgsConstructor注解用于在编译时自动生成包含特定字段的构造方法。字段筛选逻辑如下所示:被final修饰的未显式初始化的非静态字段被@NonNull注解标记的未显式初始化的非静态字段示例:import lombok.NonNull;import lombok.RequiredArgsConstructor;@RequiredArgsConstructorpublic class User { private final String name; @NonNull private Integer age; private final String address = ""; private String email; private static String city; @NonNull private String sex = "男";}以上代码在编译时自动生成的构造方法如下所示:public User(String name, @NonNull Integer age) { if (age == null) { throw new NullPointerException("age is marked non-null but is null"); } else { this.name = name; this.age = age; }}从生成的构造方法可以看出:1)如果字段被lombok.NonNull注解标记,在生成的构造方法内会做null值检查。2)address字段虽然被final修饰,但因为已初始化,所以未包含在构造方法中。3)email字段既没被final修饰,也没被lombok.NonNull注解标记,所以未包含在构造方法中。4)city字段是静态字段,所以未包含在构造方法中。5)sex字段虽然被lombok.NonNull注解标记,但因为已初始化,所以未包含在构造方法中。4. 总结@Autowired注解在IDEA中提示:Field injection is not recommended,其背后的本质问题是:Spring官方推荐构造器注入而不是字段注入。而Spring官方推荐构造器注入,是因为相比字段注入,构造器注入有以下几个优点:支持不可变性依赖明确单元测试友好循环依赖检测前置,提前暴露问题使用构造器注入时,为了避免样板化代码或者为了简化代码,可以使用@RequiredArgsConstructor注解来代替显式的构造方法,因为@RequiredArgsConstructor注解可以在编译时自动生成包含特定字段的构造方法。至于项目中要不要使用构造器注入,使用显式的构造方法还是使用@RequiredArgsConstructor注解来简化代码,可以根据个人喜好及团队规范自行决定。
  • [技术干货] @RestController和@Controller的区别
    @RestController 和 @Controller 是Spring框架中用于定义控制器(Controller)的两个非常重要的注解,它们都用于处理HTTP请求,但它们之间存在一些关键的区别。1.@Controller@Controller 注解是Spring MVC的一部分,用于定义一个控制器类。当Spring MVC接收到一个请求时,它会根据请求的URL映射到相应的控制器类上。@Controller 注解的类中的方法返回的是字符串(通常是视图名)或ModelAndView对象,这些返回值会用于渲染视图(通常是JSP页面)。@Controller 注解通常与@RequestMapping或它的变体(如@GetMapping, @PostMapping等)一起使用来定义请求处理的方法。如果你希望将MVC模式中的“控制器”部分与“视图”部分分离,并且希望由Spring MVC来管理视图的渲染,那么你应该使用@Controller。2.@RestController@RestController 是Spring 4引入的一个方便的注解,它实际上是@Controller和@ResponseBody的组合注解。它意味着,当控制器中的方法返回一个对象时,Spring会自动将这个对象转换为JSON或XML(取决于请求的Accept头部)并写入HTTP响应体中。@RestController更适合构建RESTful Web服务,因为它简化了返回JSON或XML数据的过程。使用@RestController注解的控制器类中的方法通常会返回一个对象或对象列表,而不是视图名或ModelAndView对象。@RestController也常与@RequestMapping或它的变体一起使用来定义请求处理的方法。3.注意如果你正在构建一个需要渲染视图的Web应用(如基于JSP的Web应用),那么你应该使用@Controller。如果你正在构建一个RESTful Web服务,希望直接返回JSON或XML等数据格式,那么@RestController将是更好的选择。@RestController简化了返回数据的过程,因为它自动将返回的对象转换为JSON或XML,而@Controller则需要额外的步骤来渲染视图。上代码和效果图@Controller注解@Controller@RequestMapping("/user")public class UserController { @Autowired private UserService userService; @RequestMapping("/init") public String init() { return "login"; }}@RestController注解@RestController@RequestMapping("/user")public class UserController { @Autowired private UserService userService; @RequestMapping("/init") public String init() { return "login"; }}转载自https://www.cnblogs.com/sailCoding/p/18420217
  • [技术干货] Java 中操作 Map时,高效遍历和安全删除数据
    在 Java 中操作 Map 时,高效遍历和安全删除数据可以通过以下方式实现:一、遍历 Map 的 4 种高效方式1. 传统迭代器(Iterator)Map<String, Integer> map = new HashMap<>();map.put("key1", 5);map.put("key2", 3);Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();while (iterator.hasNext()) { Map.Entry<String, Integer> entry = iterator.next(); String key = entry.getKey(); Integer value = entry.getValue(); System.out.println(key + ": " + value);}2. Java 8+ forEach + Lambdamap.forEach((key, value) -> { System.out.println(key + ": " + value);});3. 增强 for 循环(遍历 EntrySet)for (Map.Entry<String, Integer> entry : map.entrySet()) { String key = entry.getKey(); Integer value = entry.getValue(); // ...}4. Stream API(Java 8+)map.entrySet().stream() .filter(entry -> entry.getValue() > 3) // 过滤条件 .forEach(entry -> { System.out.println(entry.getKey(); });二、安全删除 Map 中的数据1. 遍历时删除Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();while (iterator.hasNext()) { Map.Entry<String, Integer> entry = iterator.next(); if (entry.getValue() < 3) { iterator.remove(); }}2. Java 8+ removeIfmap.entrySet().removeIf(entry -> entry.getValue() < 3);3. 直接删除(已知 Key)map.remove("key1");三、关键注意事项避免并发修改异常遍历时直接调用 map.remove(key) 会导致 ConcurrentModificationException,必须使用 Iterator.remove() 或 removeIf。性能优化对 HashMap,优先遍历 entrySet()(直接获取 Key-Value)。对只读操作,forEach 和 Stream 性能接近;需过滤/删除时优先用 removeIf。并发场景多线程环境下使用 ConcurrentHashMap 并结合 Iterator.remove() 或原子操作。四、完整示例代码Map<String, Integer> map = new HashMap<>(Map.of( "key1", 5, "key2", 3, "key3", 2));// 遍历并删除 value < 3map.entrySet().removeIf(entry -> entry.getValue() < 3);// 输出结果:{key1=5, key2=3}System.out.println(map);通过上述方法,可以高效且安全地操作 Java 中的 Map 数据结构。转载自https://www.cnblogs.com/sailCoding/p/18933466
  • [技术干货] java中的anyMatch和allMatch方法
    在Java的Stream API中,anyMatch和allMatch是终端操作(Terminal Operation),用于对流中的元素进行布尔值匹配检查。它们的核心区别在于匹配逻辑和短路行为:🚀1. anyMatch(Predicate)功能:检查流中是否至少有一个元素满足给定的断言条件。返回值:boolean(找到第一个匹配项时立即返回true,否则遍历完所有元素后返回false)。短路特性:具有短路能力,找到第一个匹配项后立即终止流处理。典型场景:// 检查是否有至少一个偶数List<Integer> numbers = List.of(1, 3, 5, 7, 9);boolean hasEven = numbers.stream().anyMatch(n -> n % 2 == 0); // 返回false// 检查是否有权限(用户权限列表包含"ADMIN")List<String> permissions = List.of("READ", "WRITE");boolean isAdmin = permissions.stream().anyMatch("ADMIN"::equals); // 返回false🔒2. allMatch(Predicate)功能:检查流中是否所有元素都满足给定的断言条件。返回值:boolean(发现第一个不匹配项时立即返回false,否则遍历完所有元素后返回true)。短路特性:具有短路能力,发现第一个不匹配项后立即终止流处理。典型场景:// 检查是否所有数字都是偶数List<Integer> numbers = List.of(2, 4, 6, 8);boolean allEven = numbers.stream().allMatch(n -> n % 2 == 0); // 返回true// 检查所有用户是否已激活(用户状态列表全为"ACTIVE")List<String> userStatuses = List.of("ACTIVE", "INACTIVE");boolean allActive = userStatuses.stream().allMatch("ACTIVE"::equals); // 返回false🔑核心对比特性anyMatchallMatch匹配逻辑至少一个元素匹配所有元素都必须匹配短路行为找到第一个匹配项后终止找到第一个不匹配项后终止典型返回值常见true(易满足)常见false(难满足)性能敏感场景适合快速失败的正向检查适合严格的验证场景🔐底层原理这两个方法都通过迭代流中的元素进行判断,但实现上有关键差异:anyMatch:一旦遇到true立即返回,后续元素不再处理。allMatch:一旦遇到false立即返回,后续元素不再处理。💎注意事项空流处理:对空流调用时,anyMatch返回false,allMatch返回true(符合逻辑学中的"存在量词"和"全称量词"定义)。与findAny的区别:anyMatch返回布尔值,而findAny返回Optional<T>元素。性能影响:在大数据量场景下,短路特性可以显著减少计算量。🔍代码示例: /** * 判断listA中的所有字符串是否包含listB中的所有字符串(子串匹配) * @param listA 待检查的主列表(长字符串) * @param listB 需要被包含的子串列表 * @return true 如果listB所有元素都是listA中某个元素的子串 */ public static boolean containsAllSubstrings(List<String> listA, List<String> listB) { return listB.stream() .filter(Objects::nonNull) // 过滤listB中的null元素 .allMatch(b -> listA.stream() .filter(Objects::nonNull) // 过滤listA中的null元素 .anyMatch(a -> a.contains(b)) // 检查子串存在性 ); } //注意:如果list中没有null的情况下可以不加.filter(Objects::nonNull)处理 public static void main(String[] args) { // 初始化测试数据(根据您提供的示例) List<String> listA = List.of( "READ.USER-KBN-KNJ", "READ.USER-ID.", // ...(此处省略中间元素) "READ.USER-GUIDE-MSG.", "READ.USER-ERR-MSG." ); List<String> listB = List.of( "KBN-KNJ", "USER-ID", // ...(此处省略中间元素) "GUIDE-MSG", "ERR-MSG" ); // 执行匹配检查 boolean result = containsAllSubstrings(listA, listB); System.out.println("listA是否包含listB的所有子串: " + result); }转载自https://www.cnblogs.com/sailCoding/p/18950812
总条数:739 到第
上滑加载中