-
栈的概念(Stack)栈是常见的线性数据结构,栈的特点是以先进后出的形式,后进先出,先进后出,分为栈底和栈顶栈应用于内存的分配,表达式求值,存储临时的数据和方法的调用等。例如这把枪,第一发子弹是最后发射的,第一发子弹在栈底,而最新安装上去的子弹在栈的顶部,只有将上面的子弹打完(栈顶的数据走完),最后一发子弹才会射出栈的实现栈的实现是基于简单的数组形成的,我们可以将它想象成连续的数组,而栈的顺序是由后放到前放模拟实现栈的方法:push(放入一个元素到栈中)pop(提取栈顶的一个元素,并将在其栈中消除)peek(查看栈顶的元素)size(查看栈中的大小)empty(栈中是否为空)full(栈是否满了)代码import java.util.Arrays;public class MyStack implements IStack { private int[] elem; private int top;//数组的栈顶,以及数组栈中存放元素的数量 private static final int DEFAULT_CAPACITY = 10;//这里是初始容量 public MyStack() { elem = new int[DEFAULT_CAPACITY]; top = -1;//数组下标从0开始 } @Override public void push(int item) { if (full()) { //如果满了就扩容 elem = Arrays.copyOf(elem, 2 * elem.length); } elem[++top] = item; } @Override public int pop() throws RuntimeException { try { if (empty()) { throw new RuntimeException("栈为空"); } } catch (RuntimeException e) { e.printStackTrace(); } return elem[top--];//return返回后删除栈顶的元素 } @Override public int peek() { if (empty()) { throw new RuntimeException("栈为空"); } return elem[top];//返回栈顶元素 } @Override public int size() { return top+1;//去除数组0 } @Override public boolean empty() { return top == -1; } @Override public boolean full() { return top == elem.length;//count==当前的elem的总长度为true }}队列(Queue)队列是由先进先出的线性数据结构,采用的是先进先出,后进后出,如果要插入元素的话就是从入队尾巴方向插入,而删除作为出队要在头尾删除。队列的方法模拟实现队列(双链表实现)public class MyQueue implements IQueue{ static class Queue{ public int elem; public Queue next; public Queue prev; public Queue(int elem) { this.elem = elem; } } public Queue head; public Queue last; public int size=0; @Override public void offer(int val) { Queue queue = new Queue(val); if (this.head == null) { this.head = queue; this.last = queue; ++size; return; } this.last.next=queue; this.last.prev=this.head; this.last=last.next; ++size; } @Override public int poll() { if(this.head==null){ throw new RuntimeException("没有要丢掉的队列"); } Queue cur =this.head; if(this.head.next==null){ return -1; } this.head=this.head.next; this.head.prev=null; size--; return cur.elem; } @Override public int peek() { if(this.head!=null){ return this.head.elem; } return 0; } @Override public int size() { return size; }}循环队列(循环数组实现)数组实现队列的循环需要引入一个公式(目前的下标值+1)%当前数组的长度(index+1)%array.length,下标值从0开始少一个数,当index+1就是当前的总长度时,公式后的值一定为下标0。 private int[] array; private int front; private int rear; public MyCircularQueue(int k) { array=new int[k+1]; front=0;//初始位置 rear=0; } public boolean enQueue(int value) { //入列 if(isFull()){ //这里如果容量已经满了,需要先删除后在进行插入 return false; } array[rear]=value;//rear下标获取元素 rear=(rear+1)%array.length;//rear最终循环为0下标 return true; } public boolean deQueue() { //出列 if(isEmpty()){ //为空返回false return false; } front=(front+1)%array.length;//front只需要往后走 return true; } public int Front() { if(isEmpty()){ return -1; } return array[front]; } public int Rear() { if(isEmpty()){ return -1; } //这里三木操作符判断是否为0如果为0,将rear回退到最后一个位置,不为0则-1 int temp = (rear==0)?array.length-1:rear-1; return array[temp]; } public boolean isEmpty() { return front==rear; } public boolean isFull() { return (rear+1)%array.length==front; }}用队列实现栈因为队列是先进先出的,而我们的栈是先进后出的,两种线性结构的关系是颠倒的,一个队列是不能完成的,我们需要两个队列互相工作来完成辅助队列先获取数值,保证辅助队列是最后一个拿到值的,然后将主队列的值给到辅助队列,在交换两个队列的数值,因为队列关系先进先出,每一次最后一个值就是队列先出的数值主队列不为空,将主队列的元素都poll出放到辅助栈中,使用一个tmp来将主队列(这里主队列已经遍历完)和辅助队列交换 Queue<Integer> q1;//主队列 Queue<Integer> q2;//辅助队列 public MyStack() { q1=new LinkedList<>();//构造方法 q2=new LinkedList<>(); } public void push(int x) { q2.offer(x); while(!q1.isEmpty()){//主队列不为空,则将主队列出列给到辅助队列 q2.offer(q1.poll()); } //走到这里主队列是为空 Queue tmp=q1; q1=q2; q2=tmp; //将两个队列交换 } public int pop() { return q1.poll(); } public int top() { return q1.peek(); } public boolean empty() { return q1.isEmpty(); }}用栈来实现队列栈来实现队列,栈是先进后出的顺序,而队列是先进先出的顺序将push都放到a栈中当我们peek或者是要删除的时候,我们都将a栈的元素pop给b栈,这样b栈已经有了我们的元素但是我们还需要考虑的是丢掉元素后如果在一起添加元素到a栈呢,这里我们给一个条件,如果b的栈不为空时,我们仍然用b栈的队列如果a为空,这两个栈都是空的说明没有元素直接返回-1,如果a不为空的话且b没有新的元素b继续捕获新的a栈中所有的元素class MyQueue { Stack<Integer> A; Stack<Integer> B; public MyQueue() { A=new Stack<>(); B=new Stack<>(); } public void push(int x) { A.push(x); } public int pop() { int check=peek(); B.pop(); return check; } public int peek() { //先判断b是否是空的,如果不是空的直接返回,是空才可以往下走 if(!B.isEmpty())return B.peek(); //因为b还不是空的,所以不需要将a栈放到b中 if(A.isEmpty())return -1; while(!A.isEmpty()){ B.push(A.pop());//将所有的a放到b中 } return B.peek(); } public boolean empty() { return A.isEmpty()&&B.isEmpty(); //a和b都为空才为空 }}总结栈分为栈顶和栈底,最先进的为栈底,最后进的为栈顶。队列分为队头和队尾,最先进的为队头,最后进的为队尾。————————————————原文链接:https://blog.csdn.net/weixin_60489641/article/details/143723419
-
写在前面几个Java哥们儿瞪着满屏的报错,脸都快贴屏幕上了——项目deadline催命呢,这场景,熟吧?憋屈吧?可你扭头看看隔壁组,人家正端着咖啡杯,有说有笑地做测试呢!为啥?人家刚用了个叫飞算JavaAI的东西,把整个电商平台的后端代码,“唰”一下给整出来了!乖乖,这世道,真变了?说飞算JavaAI,你可别想岔了。它不是你写代码时蹦出来的那种“小补丁”,顶多算个“单词提示”。这玩意儿是动真格的——全球头一个专门伺候Java的,能直接给你“吐”出一整套、能跑、能用的项目代码! 背后是正经搞技术的飞算公司,牛人不少,钱也厚实。它牛在哪?简单说,就是把咱原来那套写代码的苦逼流程,给“掀桌子”了。你跟它叨咕一句“弄个订单管理系统”,它吭哧吭哧就给你整出接口、数据库、业务逻辑全套家伙事儿,直接能跑!科幻片?不,现在真有兄弟在用了。为啥说这玩意儿能救命?专治各种“工伤”!跟产品经理“鸡同鸭讲”?拜拜了您嘞! 产品老哥嘴里的“用户画像”,你以为是打标签?结果他要的是猜用户下一步买啥!来回掰扯,跟传话游戏似的,心累得慌!JavaAI咋整?你直接跟它唠嗑(说话都行),它就能整明白你要啥,连你没想到的(比如商品视频咋存咋管)都能给你拎出来。沟通成本?直接砍半!烦死人的CURD“搬砖”?丢给它! 建表?写增删改查接口?配那些乱七八糟的依赖包?这些破事儿占了大把时间,干完还没啥成就感,纯纯的“工具人”!JavaAI就猛了,点一下,Maven/Gradle项目骨架、标准代码、配置文件,全套齐活! 省下的功夫,琢磨点有意思的技术难点,不香吗?早点下班陪女朋友(如果有的话)不香吗?看见老代码就想跑?它能当“老中医”! 那些用老掉牙的Hibernate写的“祖传屎山”,看着就头大,重构?跟考古挖坟没区别,生怕动一下就塌了!JavaAI自带本地“老中医”功能,能帮你把这堆老古董“号号脉”,再看看现在有啥好用的新玩意儿,给你出个靠谱的升级方案,至少心里有底了。这玩意儿到底有啥能耐?Lethehong给你盘盘道兄弟们都说它是“六边形战士”,真不是瞎吹:嘴皮子一动,设计图就来了:你就说“搞个会员积分系统”,它立马给你列出要哪些接口、数据库表长啥样,连字段啥类型、主键咋设都给你整得门儿清。它肚子里专门琢磨过Java的“脾气”,设计出来的东西,扩展性好,不容易“牵一发动全身”。复杂业务不怕翻车?它有“防呆”招儿! 搞多张表一起操作、或者一堆人同时抢资源(高并发)?心里打鼓怕出幺蛾子吧?它能把复杂的业务逻辑掰开了、揉碎了,变成一步步能走的,还提前帮你瞅瞅哪儿可能打架。更神的是,你改了点小地方,它还能偷偷把相关的逻辑也调顺溜,有效防止“改一行代码,整个系统嗝屁”的惨案(这痛,扎心不?)。代码风格看不上眼?按你的规矩来! 嫌弃生成的代码太死板、没个性?简单!你直接跟它说你们组有啥“家规”(比如“DTO必须验数据”、“不准在代码里写死数字”),它生成的代码,立马就规规矩矩按你的“家规”来,跟你们组自己人码的一模一样。老系统不敢大动?它“小刀慢割”! 面对一堆陈年老代码,2.0版本多了个“一块一块生成”的功能,贼实用。你可以挑着某个接口或者功能,单独让它生成新代码,还能马上看到效果。往老系统里塞的时候,也不用提心吊胆怕把整个系统搞崩了。谁在用?反正不是摆设!刚入行的小白:被Spring Boot那些注解绕得七荤八素?用它生成个标准项目直接跑起来,边改边看边学,比干啃教程快多了,上手贼快!被deadline追着跑的苦命团队:真有兄弟(做医疗平台的)用了,仨小时,订单模块搞定! 搁以前,吭哧吭哧手写至少三天!省下的时间,人家转头就去搞更核心的算法优化了,效率杠杠的。总被“需求误解”气哭的产品经理:这回牛了,能直接甩给开发一个“能跑”的技术方案!再也不用背锅说“我明明说的是A,你们咋做出个C?”了,腰杆都直了!想少掉点头发的技术老大(CTO):用上它的规则引擎统一代码风格,Code Review的破事儿直接少了一大半! 团队代码看起来清清爽爽,老大也省心,少熬点夜,头发能多留几根。别小看它,可能真要“变天”别人还在吵吵AI写的代码片段靠不靠谱,飞算JavaAI已经玩得更深了:它把咱们这帮写Java的,从流水线上拧螺丝的“码农”,变成了指挥AI“施工队”干活的“包工头”(架构师)。有个用了的CTO老哥说的大实话:“以前兄弟们80%的劲儿都耗在写基础代码和擦屁股(修Bug)上了,现在?能腾出手来琢磨点真正有技术含量的、创新的东西了!”飞算这家公司,野心不小。之前搞的SoFlu软件机器人就吹过“一个人就能扛一个项目,十个人能当百人用”,在银行、医院这些地方都用上了。现在这个JavaAI是他们的“大招”,配上低代码、数据工具,搞了个给企业用的“数字化套餐” ——这架势,明摆着是要抢传统软件外包公司那万亿级别的大蛋糕啊!如何安装飞算 JavaAI步骤一:从 IDEA 插件市场安装点击导航-插件,打开应用市场,搜索相关关键字"CalEx JavaAl"或"飞算"都能搜索到,找到CalEx-JavaAl 后点击安装步骤二:注册飞算 JavaAl重启 Inteli IDEA 工具,请先注册账号并登录,登录成功后即可开始体验“智能分析”、"JavaChat”"智能问答“等功能。新项目开发”一键生成完工程文档,”“老项目二次开发”本地化智能分析深度理解+自定义开发规范。 步骤三:在日常开发中使用飞算JavaAlJava AI真的有那么神? 测试项目的优缺点 这里我正好有一个项目在线,于是我就让他给我一个文件中的优缺点 优点清晰的项目结构:文件结构符合标准的Maven项目结构,易于管理和扩展。明确的依赖管理:使用Maven管理依赖,版本明确,便于维护和更新。常用的库版本:使用的MySQL驱动和MyBatis版本都是比较稳定的版本,适合大2多数应用场景。缺点JUnit作用域设置不正确:JUnit的依赖作用域被设置为compile,而通常情况下应该设置为test,以避免将测试依赖打包到生产环境中。缺少日志库依赖:项目中没有包含日志库(如SLF4J或Log4j),这在实际应用中通常是必要的,以便于调试和监控。缺少其他实用依赖:例如数据库连接池(如HikariCP)、Spring框架(如果需要事务管理和依赖注入)等,这些在实际项目中可能会提高开发效率和应用性能。 Java AI制作Java算命这里生成代码的速度已经超越了绝大部分的AI编程工具我想利用Java写一个算命的程序,请你给我给出完整的代码 package com.test; import java.util.Scanner; public class FortuneTeller { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("请输入您的出生年份:"); int birthYear = scanner.nextInt(); scanner.close(); String fortune = tellFortune(birthYear); System.out.println("您的命运预测如下:"); System.out.println(fortune); } private static String tellFortune(int birthYear) { switch (birthYear % 12) { case 0: return "您属鼠,今年可能会遇到很多新的开始和机会。"; case 1: return "您属牛,今年可能会有很多稳定和收获。"; case 2: return "您属虎,今年可能会有挑战和冒险,但也充满力量。"; case 3: return "您属兔,今年可能会有快乐和轻松,但也需要注意健康。"; case 4: return "您属龙,今年可能会有领导力和创造力的展现。"; case 5: return "您属蛇,今年可能会有智慧和耐心的增长。"; case 6: return "您属马,今年可能会有活力和速度的提升。"; case 7: return "您属羊,今年可能会有和谐和温暖的感受。"; case 8: return "您属猴,今年可能会有灵活性和适应性的增强。"; case 9: return "您属鸡,今年可能会有勤奋和节俭的习惯。"; case 10: return "您属狗,今年可能会有忠诚和友谊的支持。"; case 11: return "您属猪,今年可能会有幸福和满足的感觉。"; default: return "无法预测的命运。"; } }}飞算JavaAI vs 国内其他AI编程工具跟通义灵码比较最大的区别就是专注度通义灵码支持Java、Python、C++等多种语言,但飞算JavaAI就专门死磕Java这一门语言。这就像一个是全科医生,一个是心脏科专家的区别。我之前同时用过这两个,感受挺明显的:通义灵码生成代码时,经常给你一些通用的模板,但涉及到Java特有的一些优化和最佳实践,就显得有点浅飞算JavaAI生成的代码遵循Java代码编写的最佳实践,代码风格统一、规范,可读性强代码生成的完整度差别很大这个差别真的很明显。通义灵码在生成复杂业务逻辑代码时,生成的代码结构有时不够清晰,需要开发者花费更多时间去梳理和优化。我拿同一个需求测试过,通义灵码给我生成了几个代码片段,我还得自己组装。飞算JavaAI直接给我一套完整的工程代码,连数据库建表语句都有了。跟文心快码比较百度这个确实厉害,但思路不一样文心快码支持超过100种主流编程语言,覆盖了从系统编程到Web开发、移动应用开发等多个领域。功能很全面,但问题还是老毛病——太泛了。我试过用文心快码做个电商系统,它能理解我的需求,也能给代码,但给的都是一些标准的CRUD操作。想要一些高级功能,比如分布式锁、缓存策略这些,就比较吃力。飞算JavaAI专注于Java单一语言开发,对Java语言特性和编程规范有深入理解,能生成高质量、符合行业最佳实践的Java代码。在处理复杂业务逻辑时,它真的能生成结构清晰、逻辑严谨的代码。跟豆包MarsCode比较字节的这个工具我用得不多主要原因是实测下来,感觉和GitHub Copilot和通义灵码都有差距,说实话是有点失望的。可能是因为发布时间比较晚,还在持续优化中。不过豆包MarsCode有个优势是它除了编程助手,还提供了云端开发环境。但纯粹从代码生成质量来说,跟飞算JavaAI比还是有明显差距的。实际使用建议如果你是:Java专业开发者:强烈推荐飞算JavaAI,真的能大幅提升效率多语言开发者:可以考虑通义灵码或文心快码个人学习者:通义灵码免费,可以先试试企业级项目:飞算JavaAI在代码质量和完整性上更有保障说实话,用过飞算JavaAI之后,再用其他工具总感觉缺点什么。就像习惯了自动挡汽车,再开手动挡总觉得麻烦。当然,这也可能是因为我主要做Java开发的原因。不过有一点要说明,飞算JavaAI目前主要专注后端,如果你要做前端开发,可能还是得配合其他工具使用。写在最后凌晨的办公室,咖啡机还在那儿“咕噜咕噜”响。但原来那密集的键盘“交响乐”少了,多了点飞算JavaAI干活时那种低沉的“嗡嗡”声。一个开发兄弟指着屏幕,乐了:“搞定!订单退款逻辑跑通了,嘿,连测试多人同时退款的代码都给我备好了!” 他那组人已经开始收拾包,张罗着去吃宵夜了。为啥这么潇洒?因为明天产品要的新需求讨论,他们今晚就能把演示版(Demo)整出来。当AI把那些重复的、费脑子的“搬砖”活儿扛了,咱们这帮写Java的脑子,总算能腾出来,干点更带劲、更有创造性的活儿了——比如,想想宵夜点啥烤串? (或者,早点回家睡觉?)————————————————原文链接:https://blog.csdn.net/2301_76341691/article/details/148697903
-
一、Java发展史 Java最初由Sun公司的“Green”项目组开发,用于智能家电设备,最初名为Oak。因商标问题,1995年更名为“Java”(灵感源于印尼爪哇岛的咖啡)。发行版本 发行时间 发行的各版本及其特征Java 1995年 Java语言诞生Java 1.0 1996年 首个正式版本,包含基础类库和Applet支持Java 1.1 1997年 引入内部类(Inner Class)、Java Beans、JDBC(数据库连接)和反射APIJava 1.2 1998年 JDK 1.2发布,更名为Java 2,分为三个平台:J2SE(标准版)、J2EE(企业版)、J2ME(微型版)Java 1.3 2000年 引入HotSpot JVM、JNDI(Java命名与目录接口)Java 1.4 2002年 新增正则表达式、断言(Assert)、NIO(非阻塞I/O)和日志APIJava 5.0 2004年 引入泛型、注解、枚举等革命性特性,为强调版本重要性,Sun将内部版本号1.5公开命名为5.0,此后版本号逐渐简化Java 6.0 2006年 Sun将产品线更名为Java SE/EE/ME,终结“J2”前缀,并宣布开源(OpenJDK)2009年 Oracle以74亿美元收购财务困境的Sun公司,Java正式归属OracleJava 7.0 2011年 Oracle首个大版本,支持菱形语法、多异常捕获,但因收购过渡期特性较少Java 8.0 2014年 继JDK 5后最大更新,引入Lambda表达式、Stream API、新日期时间库。LTS(长期支持)版本Java 9.0 2017年 发布周期改为每半年发布一次版本,每三年推出LTS(长期支持)版本Java 10.0 2018年 废弃“1.x”格式,直接使用主版本号(如JDK 10而非JDK 1.10)Java EE移交Eclipse基金会,重命名为Jakarta EE(如包名从javax.*改为jakarta.*)Java 11.0 2018年 新增HTTP客户端API、局部变量类型推断(var)并移除部分过时功能。LTS(长期支持)版本… … Java21.0 2023年 被视为继Java 8后的新一代主流版本,生态支持(如框架适配率)快速提升。LTS(长期支持)版本二、Java技术体系平台1、JavaSEJavaSE 的全称是 Java Platform Standard Edition(Java 平台标准版)面向桌面级应用(如Windows下的应用程序),提供完整的Java核心API,是其他平台(JavaEE、JavaME)的基础JavaSE和JDK的关系JavaSE(规范):定义接口、抽象类、具体类以及JVM的行为和约束(定义语言和API应该是什么样)例:JavaSE规范要求必须有一个ArrayList类,它实现List接口,支持动态扩容JDK(实现):提供这些接口和类的具体代码实现(按照规则实现并提供开发工具和运行环境)例1:OracleJDK的ArrayList源码中,具体实现了扩容机制(如默认扩容1.5倍)例2:OpenJDK的ArrayList可能实现相同的逻辑,但代码细节可能有细微差异(如注释、内部优化)历史名称:早期称为J2SE(JDK 6之前)2、JavaEEJavaEE 的全称是 Java Platform Enterprise Edition(Java 平台企业版)在Java SE基础上扩展了大量企业级API(如Servlet、JSP、EJB),提供分布式计算、事务管理、安全性等企业级功能JavaEE接口由官方规范定义,具体实现由应用服务器(Tomcat、WildFly)或第三方库(Hibernate、ActiveMQ)提供自JDK 10起由Oracle移交Eclipse基金会管理,更名为Jakarta EE历史名称:曾用名J2EE(JDK 6之前)3、JavaMEJavaME 的全称是 Java Platform Micro Edition(Java 平台微型版)针对移动终端(手机、PDA等)的轻量级平台,精简了Java SE的API并加入移动设备支持随着 Android 和 iOS 的普及,JavaME 的使用逐渐减少历史名称:曾用名J2ME4、三者关系JavaSE 是基础:JavaEE 和 JavaME 均基于 JavaSE 的核心功能构建JavaEE 是扩展:在 JavaSE 基础上增加企业级服务规范(如 Servlet、JPA、EJB)JavaME 是精简:仅保留 JavaSE 部分功能,并添加针对微型设备的特性三、Java程序运行机制及运行过程1、Java的跨平台性2、Java虚拟机(核心机制)JVM 是一个虚拟的计算机,具有指令集并使用不同的存储区域。负责执行指令,管理数据、内存、寄存器,包含在JDK 中对于不同的平台,有不同的虚拟机Java 虚拟机机制屏蔽了底层运行平台的差别,实现了“一次编译,到处运行”四、Java语言环境搭建1、JDK(Java开发工具包)定义:JDK是用于开发Java应用程序的完整工具包,包含编译、调试、文档生成等开发工具以及运行环境组成部分:JRE:JDK中内置了JRE(包含核心类库),确保开发时可以直接运行程序开发工具:如编译器javac(将Java源代码编译为字节码)、调试器jdb、文档工具javadoc等JDK特有的工具类库:如:tools.jar,支持编译器(javac)、调试器(jdb)等工具的运行(位于JDK的lib目录下)用途:开发者必须安装JDK,才能编写、编译和调试Java程序2、JRE(Java运行时环境)定义:JRE是运行已编译Java程序所需的最小环境,无需开发功能组成部分:JVM(Java虚拟机) :负责执行字节码,实现跨平台特性JRE中的核心类库:以java.*包的形式存在,例如rt.jar、resource.jar下java.lang、java.util等(位于JRE的lib目录下,并由BootstrapClassLoader自动加载)JRE中的扩展类库:以javax.*包的形式组织,例如javax.sql等(JRE的lib/ext目录下,由ExtensionClassLoader加载)用途:普通用户只需安装JRE即可运行Java程序(如.jar或.class文件),无需开发工具3、环境变量及作用3.1、JAVA_HOME该环境变量的值是Java的安装路径,一些Java版本的软件和工具需要用到该变量例如,当Windows平台上JDK的安装目录为“C:\java\jdk8”时,设置如下所示JAVA_HOME=C:\java\jdk813.2、CLASSPATH该环境变量用于指明Java字节码文件(.class文件)的位置默认情况下,如果未设置CLASSPATH,Java启动JVM后,会在当前目录下寻找字节码文件,一旦设置了CLASSPATH,JVM会在指定目录下查找字节码文件环境变量CLASSPATH的值一般为一个以分号“;”作为分隔符的路径列表,设置如下CLASSPATH=.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar;1“.”表示当前目录,因为设置CLASSPATH会覆盖JVM的默认操作(查找当前目录),所以这里需要加上“.”dt.jar是 Java 开发工具包(JDK)中用于为 IDE 提供 Swing/AWT 组件的设计时元数据(如属性、事件描述),支持通过拖拽和图形化界面进行可视化开发的核心类库文件tools.jar的作用:包含编译工具(如javac)所需的类库Java5之前,若用户未显式配置CLASSPATH环境变量JVM不会在当前目录查询.class文件,所以需要配置CLASSPATH但从Java 5(2004年发布)开始,默认情况,无需显式配置CLASSPATH,JVM会自动搜索当前目录和核心类库3.3、PATH该环境变量指定一个路径列表,用于搜索可执行文件执行一个可执行文件时,如果该文件不能在当前路径下找到,则依次寻找PATH中的每一个路径,直至找到。例如:PATH=.;%JAVA_HOME%\bin;1这样可以在命令行中直接使用java和javac命令,而不需要指定完整路径,否则就会出现以下错误:不建议在PATH环境变量中添加当前目录"."的主要原因:如果当前目录"."被加入PATH,当用户进入公共可写目录/tmp时,攻击者可能在该目录下放置与系统命令同名的恶意程序例如:黑客在/tmp目录下创建名为ls的木马文件,当用户(尤其是root用户)执行ls命令时,会优先执行当前目录下的恶意程序而非系统标准的/bin/ls,导致权限泄露或数据被破坏————————————————原文链接:https://blog.csdn.net/qq_35512802/article/details/148105022
-
最近在复习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
-
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
-
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
-
找到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
-
五、对象的构造及初始化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的世界里,我们每天都在与整数打交道: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 内存泄漏问题时,需要综合利用监控工具、日志分析、堆转储(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
-
一、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
-
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
-
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
-
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
-
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注解来简化代码,可以根据个人喜好及团队规范自行决定。
上滑加载中
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签