WWW.YOUINFO.SITE
标签聚合 JAVA

/tag/JAVA

LinuxDo 最新话题 · 2026-06-11 22:42:04+08:00 · tech

本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 LINUX DO 社区: 是 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出 前言 本教程的环境基于 jdk8 + langchain4j 0.35 教程源码放在这里了: github.com GitHub - worenbudaoni/rag-study-helper: 一个学习检索增强生成的全流程助手 一个学习检索增强生成的全流程助手 文章内容 因为内容比较多,我会从下面三个文章进行讲解,后续发布后会贴出来,这节讲: 接入飞书WIKI文档 RAG实现全流程 : 【开源、教程】RAG全流程实现(java+完整代码):第一弹 接入飞书WIKI文档 : 【开源、教程】RAG全流程实现(java+完整代码):第二弹 接口限流:令牌桶 + AOP 强烈建议先看完第一弹,不然后面代码有可能看不懂 实现逻辑 后面会有图文讲解的,这里就相当于大概介绍一下,看个大概就好,有不了解的不要先去搜,我后文都会讲,如果讲漏了麻烦评论一下,我改正 飞书开发者平台 : 1、去飞书开发者平台创建一个应用 2、给应用赋予权限(权限管理 菜单) 3、给应用赋予机器人能力(添加应用能力 菜单) 4、发布(版本管理与发布 菜单) 5、获取应用的app-id、app-secret(凭证和基础信息 菜单) 飞书app : 1、创建一个群聊 2、把机器人给拉进去 3、点击左边菜单的更多找到知识库,新建知识库(下面统一称为 WIKI) 4、添加群聊(机器人)为管理员:点 WIKI 进去会打开一个网站,左下角有个设置点进去,在成员设置->角色与权限->管理员,添加管理员,搜索我们刚才创建的有机器人的群聊并添加 5、在页面的连接处找到space-id,如: https://kcnvw23rzo5r.feishu.cn/wiki/settings/666666(666666就是我们要的space-id) 项目 : 1、创建一个job,用来定时获取文档(下面为job启动后的流程) 2、通过app-id、app-secret获取tenant_access_token和expire 3、通过tenant_access_token和space-id获取文档信息(名字、更新时间、documentToken 等等) 4、通过documentToken去获取文档的内容(字符串) 5、走 【开源、教程】RAG全流程实现(java+完整代码):第一弹 的入库流程 一、飞书开发者平台 app-id、app-secret 是啥 app-id :应用的唯一标识 app-secret :应用的密钥,在创建应用时由平台生成,可用于获取app_access_token 1、飞书开放平台创建企业应用 开发者后台 - 飞书开放平台 这里注意的是创建好应用后需要审核、启用,所以企业级的应用权限在领导手上,我们可以创建一个个人版的飞书账号来做实验 2、给应用赋予权限 (权限管理菜单) 直接复制我的也行 { "scopes": { "tenant": [ "bitable:app:readonly", "docx:document:readonly", "drive:drive:readonly", "drive:file:readonly", "wiki:wiki:readonly" ], "user": [] } } 3、给应用赋予机器人能力 (添加应用能力菜单) 4、发布 (版本管理与发布菜单) 创建好后发布就行 5、获取应用的app-id、app-secret(凭证和基础信息菜单) 二、飞书app space-id是啥 就是飞书知识库(WIKI)所对应的空间ID,我们找到这个空间就可以找到下面的文档 打个比方就是图书馆的书架,书架有个唯一标识(小说),我们根据这个书架ID(小说)去找下面所有的书 1、创建一个群聊 2、把机器人给拉进去 3、点击左边菜单的更多找到知识库,新建知识库(下面统一称为 WIKI) 4、添加群聊(机器人)为管理员 点 WIKI 进去会打开一个网站,左下角有个设置点进去,在成员设置->角色与权限->管理员,添加管理员,搜索我们刚才创建的有机器人的群聊并添加 5、在页面的连接处找到space-id 如: https://kcnvw23rzo5r.feishu.cn/wiki/settings/666666(666666就是我们要的space-id) 三、项目 这里就不按照上面实现逻辑写的走了,我就按照代码里的讲解 1、job总览(步骤拆解在后面) FeishuSyncService.java // 没想加重框架,如果用xxl-job什么的,自己搬一下就行了 @Scheduled(cron = "${app.feishu.cron}") public void syncWiki() { log.info("Starting Feishu wiki sync for space: {}", spaceId); try { // 获取全部文档信息(通过app-id、app-secret、space-id) // 这里逻辑后面会讲 List<WikiNode> nodes = feishuClient.getWikiNodeTree(spaceId); log.info("Found {} nodes in wiki", nodes.size()); // 成功数量,跳过数量(如果数据存在了关系型数据库且没有更新就跳过),失败数量 int synced = 0, skipped = 0, failed = 0; for (WikiNode node : nodes) { // 后缀 String objType = node.getObjType(); // 文档令牌 用来获取 文档内容 String nodeToken = node.getNodeToken(); // 更新时间判断是否需要跳过 long updateTime = node.getUpdateTime(); // 是否入库 Documents doc = documentsMapper.selectOne( Wrappers.<Documents>lambdaQuery() .eq(Documents::getFeishuNodeToken, nodeToken) ); // 文档是否更新是否需要跳过,这个更新时间不在where条件里面是因为后续要继续用到这个数据 if (doc != null && doc.getFeishuUpdateTime() != null && doc.getFeishuUpdateTime() == updateTime) { skipped++; continue; } try { // 文档内容 String content; // 获取文件名 String fileName; switch (objType) { case "doc": case "docx": // 获取文档内容 // 这里逻辑就不讲了,我后面扔给飞书的文档,照着对接或者看我源码就好 content = feishuClient.getDocumentContent(node.getObjToken()); fileName = node.getNodeTitle() + "_文档"; break; case "sheet": content = feishuClient.getSheetContent(node.getObjToken()); fileName = node.getNodeTitle() + "_表格"; break; case "bitable": content = feishuClient.getBitableContent(node.getObjToken()); fileName = node.getNodeTitle() + "_多维表格"; break; default: skipped++; continue; } // 如果是更新,先删旧向量和映射记录 if (doc != null) { // 查询旧文档相关的向量映射 List<DocumentChunks> oldChunks = documentChunksMapper.selectList( Wrappers.<DocumentChunks>lambdaQuery() .eq(DocumentChunks::getDocumentId, doc.getId()) ); // 有两张表 // 第一张为文档库:记录文档标题、更新时间、创建人等信息 // 第二张为分片库:记录向量数据库插入后的向量ID // 向量ID List<String> vectorIds = oldChunks.stream() .map(DocumentChunks::getVectorId) .collect(Collectors.toList()); // 删除向量 embeddingStore.removeAll(vectorIds); // 删除映射记录 documentChunksMapper.delete( Wrappers.<DocumentChunks>lambdaQuery() .eq(DocumentChunks::getDocumentId, doc.getId()) ); // 删除文档 documentsMapper.deleteById(doc.getId()); } // RAG 入库流程 (第一篇文章中亦有记载(跟第一章代码有些许出入,看完第一章后,直接看源码更佳)) ingestionService.ingestFeishuDocument(fileName, content, nodeToken, updateTime, objType); synced++; log.info(" Synced: {} ({})", node.getNodeTitle(), nodeToken); } catch (Exception e) { log.error(" Failed to sync node: {} ({})", node.getNodeTitle(), nodeToken, e); failed++; } } // 清理远程已删除的文档 // 这里的逻辑是 // 第一次job执行:查询飞书wiki给了 A、B、C 三个文档入库 // 后面有人在wiki中删了 C 文档 // 第二次job执行:只有查询出 A、B 两个文档 // 这时就要去数据库中和向量库中删除多余的 C 文档 List<String> remoteTokens = nodes.stream() .map(WikiNode::getNodeToken) .collect(Collectors.toList()); if (!remoteTokens.isEmpty()) { // MySQL 查出本地多出的记录,只遍历需要删除的 List<Documents> toRemove = documentsMapper.selectList( Wrappers.<Documents>lambdaQuery() .isNotNull(Documents::getFeishuNodeToken) .notIn(Documents::getFeishuNodeToken, remoteTokens) ); for (Documents removed : toRemove) { log.info("Document removed remotely, cleaning up: {} ({})", removed.getDocumentName(), removed.getFeishuNodeToken()); List<DocumentChunks> chunks = documentChunksMapper.selectList( Wrappers.<DocumentChunks>lambdaQuery() .eq(DocumentChunks::getDocumentId, removed.getId()) ); List<String> vectorIds = chunks.stream() .map(DocumentChunks::getVectorId) .collect(Collectors.toList()); // 向量数据库 删 embeddingStore.removeAll(vectorIds); // 关系型数据库 分片库 删 documentChunksMapper.delete( Wrappers.<DocumentChunks>lambdaQuery() .eq(DocumentChunks::getDocumentId, removed.getId()) ); // 关系型数据库 文档库 删 documentsMapper.deleteById(removed.getId()); } } log.info("Feishu wiki sync complete: synced={}, skipped={}, failed={}", synced, skipped, failed); } catch (Exception e) { log.error("Feishu wiki sync failed", e); } } 2、递归获取知识库所有文档节点 FeishuClient.java 这里其实没什么特别好讲的点,就是参考飞书文档,然后请求并解析 我在源码里也标记了文档的地址,所以这里放一个总体的查询地址 开发文档 - 飞书开放平台 /** * 获取 tenant_access_token(内部自动缓存和刷新) */ public synchronized String getAccessToken() throws IOException { if (cachedToken != null && System.currentTimeMillis() < tokenExpireAt) { return cachedToken; } String json = "{\"app_id\":\"" + appId + "\",\"app_secret\":\"" + appSecret + "\"}"; // https://open.feishu.cn/document/server-docs/authentication-management/access-token/tenant_access_token_internal Request request = new Request.Builder() .url(baseUrl + "/open-apis/auth/v3/tenant_access_token/internal") .post(RequestBody.create(JSON, json)) .build(); try (Response resp = httpClient.newCall(request).execute()) { JsonNode body = objectMapper.readTree(resp.body().string()); if (body.get("code").asInt() != 0) { throw new IOException("Failed to get access token: " + body); } cachedToken = body.get("tenant_access_token").asText(); // tenant_access_token 的最大有效期是 2 小时 // 7200 是秒 int expire = body.get("expire").asInt(7200); // 防御性编程 免得刚好过期 由于网络延时 造成接口调用失败 tokenExpireAt = System.currentTimeMillis() + (expire - 60) * 1000L; return cachedToken; } } /** * 递归获取知识库所有文档节点。 */ public List<WikiNode> getWikiNodeTree(String spaceId) throws IOException { List<WikiNode> allNodes = new ArrayList<>(); collectNodes(spaceId, null, allNodes); return allNodes; } private void collectNodes(String spaceId, String parentNodeToken, List<WikiNode> result) throws IOException { List<WikiNode> currentLevelNodes = new ArrayList<>(); String pageToken = null; do { // https://open.feishu.cn/document/server-docs/docs/wiki-v2/space-node/create StringBuilder url = new StringBuilder(baseUrl + "/open-apis/wiki/v2/spaces/" + spaceId + "/nodes"); if (parentNodeToken != null) { url.append("/").append(parentNodeToken).append("/children"); } url.append("?page_size=50"); if (pageToken != null) { url.append("&page_token=").append(pageToken); } Request request = new Request.Builder() .url(url.toString()) .header("Authorization", "Bearer " + getAccessToken()) .get() .build(); try (Response resp = httpClient.newCall(request).execute()) { JsonNode body = objectMapper.readTree(resp.body().string()); if (body.get("code").asInt() != 0) { log.error("Wiki API error for URL [{}]: {}", url, body); break; } JsonNode items = body.path("data").path("items"); for (JsonNode item : items) { WikiNode node = new WikiNode(); // 节点token node.setNodeToken(item.path("node_token").asText()); // 对应文档类型的token,可根据 obj_type 判断属于哪种文档类型。 node.setObjToken(item.path("obj_token").asText()); // 文档类型,对于快捷方式,该字段是对应的实体的obj_type。 // 可选值有: // doc:旧版文档 sheet:表格 mindnote:思维导图 bitable:多维表格 file:文件 docx:新版文档 slides:幻灯片 node.setObjType(item.path("obj_type").asText()); // 文档标题 node.setNodeTitle(item.path("title").asText()); node.setParentNodeToken(parentNodeToken); // 是否有子节点 node.setHasChild(item.path("has_child").asBoolean(false)); // 文档最近编辑时间 String editTime = item.path("obj_edit_time").asText(); node.setUpdateTime(Long.parseLong(editTime.isEmpty() ? "0" : editTime)); currentLevelNodes.add(node); } pageToken = body.path("data").path("page_token").asText(null); } } while (pageToken != null && !pageToken.isEmpty()); // Add all nodes from this level, then recurse into children result.addAll(currentLevelNodes); for (WikiNode node : currentLevelNodes) { if (node.isHasChild()) { collectNodes(spaceId, node.getNodeToken(), result); } } } 四、测试 1、导入文档 还是拿这个 补鸡稻 作为测试案例 2、配置 app-id、app-secret、space-id和sync-enable 我这里通过 jvm 运行参数注入,免得又把 apikey 给上传到 github 了 sync-enable 记得为 true,不然不注册 spring bean 3、运行时发现文档可以查询到,并且入库了 4、提问 5、测试结束,完结撒花 2 个帖子 - 2 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-11 17:14:50+08:00 · tech

楼主目前是双非一本的计算机专业研究生,主要技术栈走的是 Java 后端 。目前毕业论文基本上7月份就能做好初稿,导师的意思是初稿过审就能放我们实习。 楼主还是想趁着秋招找个实习的,看着今年这个就业市场,再看看自己已经荒废许久的传统手艺活(bushi,焦虑感直接拉满了。 想请教一下各位天才程序员佬友们,我接下来的复习路线到底该怎么走。 1.关于技术栈 现在看网上的面经,感觉什么都要精通。请问下各位佬: 常规八股文 (Java并发、Spring全家桶)这个看起来是要狠狠背诵的,要背到什么程度?或者各位佬友有没有八股文秘籍资源 MySQL、Redis 以及各类中间件 现在面试卷到什么程度了?是光会用就行,还是必须把底层原理也能讲出来(如果那样的话)? 最近看 Spring AI 这种AI或者Agent相关都已经几乎成后端必会的东西了,这玩意儿重要性怎么样?还是说建议做个集成AI的个人项目会好一些? 2.关于力扣刷题 之前楼主没有参与过算法类竞赛啊,不知道刷这个题库对找工作效果怎么样啊,要刷到什么程度啊?我看身边很多同学都在刷,是只刷完Hot 100就行么? 3.最核心的问题:复习优先级与掌握程度 如果以冲刺今年秋招、争取拿到一个性价比不错的Offer为目标,从现在开始到秋招结束,我应该怎么安排学习顺序? 能不能请各位天才程序员帮我排个 重要度梯队 ?(例如:最优先学什么、学到什么程度;其次学什么……) 平时在L站看各位佬友吹水技术收获颇丰,这次真到了自己面临人生节点的时候,希望各位天才程序员们能解答一下啊 1 个帖子 - 1 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-11 10:54:19+08:00 · tech

其他内容参见 JAVA学习记录总贴 本期是oop的简单练习,确实都很简单,主要负责熟手 第一题 public static void main(String[] args) { Car c = new Car(); Car car = new Car(100); System.out.println(c); System.out.println(car); } ​ class Car { double price = 10; static String color = "white"; ​ public String toString() { return price + "\t" + color; } ​ public Car(){ this.price = 9; this.color = "black"; } ​ public Car(double price){ this.price = price; } } 答案: 9.0 black 100.0 black 分析: 这道题应该挺好理解的。 ​ 注意 color 是一个静态变量,在第一次 new Car 的时候,static变量 就已经被修改了。 ​ 所以,最后两个实例的静态变量都是 black。 第二题 不难,纯粹练手 在Frock类中声明私有的静态属性currentNum[int类型],初始值为100000,作为衣服出厂的序列号起始值。 声明公有的静态方法getNextNum,作为生成上衣唯一序列号的方法。每调用一次,将currentNum增加100,并作为返回值。 在Homework02类的main方法中,分两次调用getNextNum方法,获取序列号并打印输出。 在Frock类中声明serialNumber(序列号)属性,并提供对应的get方法。 在Frock类的构造器中,通过调用getNextNum方法为Frock对象获取唯一序列号,赋给serialNumber属性。 在Homework02类的main方法中,分别创建三个Frock 对象,并打印三个对象的序列号,验证是否为按100递增。 答案: package hspedu.homework; ​ public class Homework02 { public static void main(String[] args) { System.out.println(Frock.getNextNum()); System.out.println(Frock.getNextNum()); ​ Frock f1 = new Frock(); System.out.println(f1.getSerialNumber()); ​ Frock f2 = new Frock(); System.out.println(f2.getSerialNumber()); ​ Frock f3 = new Frock(); System.out.println(f3.getSerialNumber()); } } ​ class Frock { private static int currentNum = 100000; private int serialNumber; ​ public static int getNextNum() { currentNum += 100; return currentNum; } ​ public Frock() { serialNumber = getNextNum(); } ​ public int getSerialNumber() { return serialNumber; } } 第三题 一个简单的多态练习,题干如下 动物类Animal包含了抽象方法 shout(); Cat类继承了Animal,并实现方法shout,打印“猫会喵喵叫” Dog类继承了Animal,并实现方法shout,打印“狗会汪汪叫” 在测试类中实例化对象Animal cat = new Cat(),并调用cat的shout方法 在测试类中实例化对象Animal dog = new Dog(),并调用dog的shout方法 答案: public class Homework03 { public static void main(String[] args) { Animal cat = new Cat(); cat.shout(); Animal dog = new Dog(); dog.shout(); } } ​ abstract class Animal{ abstract void shout(); } ​ class Cat extends Animal{ @Override void shout() { System.out.println("猫会喵喵叫"); } } ​ class Dog extends Animal{ @Override void shout() { System.out.println("狗会汪汪叫"); } } 第四题 计算器接口具有 work 方法,功能是运算,有一个手机类 Cellphone,定义方法 testWork 测试计算功能,调用计算接口的 work 方法 要求调用 CellPhone 对象 的 testWork 方法,使用上匿名内部类 答案: ps:设计匿名内部类,建议还是认真写一下 public class Homework04 { public static void main(String[] args) { Cellphone cellphone = new Cellphone(); Calculator cal = new Calculator() { @Override public void work(int a, int b) { System.out.println(a + b); } }; cellphone.testWork(cal, 7, 8); } } ​ interface Calculator { void work(int a, int b); } ​ class Cellphone { public void testWork(Calculator calculator, int a, int b) { System.out.println("测试计算功能"); calculator.work(a, b); } } 第五题 编一个类 A,在类中定义局部内部类 B,B 中有一个私有常量 name,有一个方法 show () 打印常量 name。进行测试 进阶:A 中也定义一个私有的变量 name,在 show 方法中打印测试 答案:a piece of cake public class Homework05 { public static void main(String[] args) { new A().new B().show(); } } ​ class A{ private final String name = "AAA"; class B{ private final String name = "BBB"; public void show(){ System.out.println(name); System.out.println(A.this.name); } } } 第六题 有一个交通工具接口类Vehicles,有work接口 有Horse类和Boat类分别实现Vehicles 创建交通工具工厂类,有两个方法分别获得交通工具Horse和Boat 有Person类,有name和Vehicles属性,在构造器中为两个属性赋值 实例化Person对象“唐僧”,要求一般情况下用Horse作为交通工具,遇到大河时用Boat作为交通工具 额外:使用匿名内部类,增加一个用飞机过火焰山的方法,因为只用一次,因此不要写一个新的类 答案: public class Homework06 { public static void main(String[] args) { Person tang = new Person("唐僧", null); tang.passRiver(); tang.common(); tang.passRiver(); tang.passMountain(); } } ​ interface Vehicles { void work(); } ​ class Horse implements Vehicles { @Override public void work() { System.out.println("horse is working"); } } ​ class Boat implements Vehicles { @Override public void work() { System.out.println("boat is working"); } } ​ class Plane implements Vehicles { @Override public void work() { System.out.println("plane is working"); } } ​ class Factory { private static final Horse HORSE = new Horse(); private static final Boat BOAT = new Boat(); private static final Plane PLANE = new Plane(); ​ public static Horse getHorse() { return HORSE; } ​ public static Boat getBoat() { return BOAT; } ​ public static Plane getPlane() { return PLANE; } } ​ class Person { private String name; private Vehicles vehicle; ​ public Person(String name, Vehicles vehicle) { this.name = name; this.vehicle = vehicle; } ​ public void setVehicle(Vehicles vehicle) { this.vehicle = vehicle; } ​ ​ public void passRiver() { if (!(vehicle instanceof Boat)) { vehicle = Factory.getBoat(); } vehicle.work(); } ​ public void common() { if (!(vehicle instanceof Horse)) { vehicle = Factory.getHorse(); } vehicle.work(); } ​ public void passMountain() { vehicle = new Vehicles(){ @Override public void work() { System.out.println("plane is working"); } }; vehicle.work(); } } 简要分析: 用懒汉式单例,保证只创建一个船和马对象,节省资源 用 vehicle instanceof Horse 来判断具体情况 第七题 有一个 Car 类,有属性 temperature(温度),车内有 Air(空调)类,有吹风的功能 flow,Air 会监视车内的温度,如果温度超过 40 度则吹冷气。如果温度低于 0 度则吹暖气,如果在这之间则关掉空调。实例化具有不同温度的 Car 对象,调用空调的 flow 方法,测试空调吹的风是否正确。 public class Homework07 { public static void main(String[] args) { Car car = new Car(42); Car.Air air = car.getAir(); air.flow(); Car car1 = new Car(2); Car.Air air1 = car1.getAir(); air1.flow(); Car car2 = new Car(-1); Car.Air air2 = car2.getAir(); air2.flow(); } } ​ class Car{ private double temperature; ​ public Car(double temperature) { this.temperature = temperature; } ​ class Air{ public void flow(){ if(temperature > 40){ System.out.println("吹冷风"); } else if(temperature < 0){ System.out.println("吹暖风"); } else { System.out.println("关闭空调"); } } } ​ public Air getAir(){ return new Air(); } } 第八题 创建一个Color枚举类 有 RED,BLUE,BLACK,YELLOW,GREEN这五个枚举值/对象; Color有三个属性redValue, greenValue, blueValue, 创建构造方法,参数包括这三个属性, 每个枚举值都要给这三个属性赋值,三个属性对应的值分别是 red: 255,0,0 blue:0,0,255 black:0,0,0 yellow:255,255,0 green:0,255,0 定义接口,里面有方法show,要求Color实现该接口 show方法中显示三属性的值 将枚举对象在switch语句中匹配使用 1 个帖子 - 1 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-11 00:18:55+08:00 · tech

以前ai生成完代码,我还会打开idea看一遍,提交前也会习惯性地在idea里跑一下项目。但后来工作量越来越大,ai也越来越强,现在基本上一天就是codex一把梭,idea都很少打开了。 目前还离不开idea的地方主要有两个:一个是git分支管理,之前用idea的分支切换、合并这些操作习惯了,偶尔分支乱了还是会点开idea处理一下;另一个是项目启动,有时候担心ai改的代码有问题,还是会用idea跑起来看一眼。 所以想问问大家,现在的工作工具都是怎么搭配的?我其实有点想完全抛弃idea,但git管理和项目启动这两块还没找到特别顺手的替代方案,想看看大家有没有什么好方法可以借鉴。 14 个帖子 - 10 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-10 16:14:20+08:00 · tech

枚举和注解 枚举 基础知识 枚举是一组常量的集合。枚举属于一种特殊的类,里面只包含一组有限的特定的对象。 其实枚举类是可以通过传统写法自定义的,写法为: 构造器私有化 不提供set方法 在类内部预先初始化好静态的实例,并且对外暴露 代码略,直接学习如何创建真正的枚举。 使用enum关键字来代替class 直接写FALL(“秋天”,“凉爽”),效果上等价于 public static final Season FALL = new Season(“秋天”,“凉爽”); 如果有多个常量对象,使用逗号间隔即可 使用 enum 实现枚举,必须把定义的常量对象写在枚举类的最前面 使用无参构造器时,可以把括号也省略,直接写FALL,举例 FALL, SPRING, SUMMER, WINTER; public class Enum { public static void main(String[] args) { System.out.println(Season.SPRING); } } enum Season{ SPRING("春天","温暖"), SUMMER("夏天","炎热"), FALL("秋天","凉爽"), WINTER("冬天","寒冷"); private String name; private String desc; Season(String name, String desc) { this.name = name; this.desc = desc; } @Override public String toString() { return "Season{" + "name='" + name + '\'' + ", desc='" + desc + '\'' + '}'; } } .java文件可以用 Javac 编译成.class文件,.class文件也可以用 Javap 反编译成字节码文件,通过观察先编译再反编译的结果,可以看到很多隐藏的细节。 对于 Seanon 类,反编译得到的代码如下 Compiled from "Enum.java" final class hspedu.inner.enumer.Season extends java.lang.Enum<hspedu.inner.enumer.Season> { public static final hspedu.inner.enumer.Season SPRING; public static final hspedu.inner.enumer.Season SUMMER; public static final hspedu.inner.enumer.Season FALL; public static final hspedu.inner.enumer.Season WINTER; public static hspedu.inner.enumer.Season[] values(); public static hspedu.inner.enumer.Season valueOf(java.lang.String); public java.lang.String toString(); static {}; } 值得关注的细节有: 枚举类是 final 类型的,因此不可被继承 枚举类默认继承 java.lang.Enum 类,因此不可继承其他类 每一个常量都默认是 public static final 类型的 简单练习 第一题 判断语法正误 enum Gender { BOY, GIRL; } 答案: 语法没有错,相当于一个“没有属性,只含有无参构造器”的枚举类 第二题 判断输出什么 enum Gender2 { BOY, GIRL; } Gender2 boy = Gender2.BOY; Gender2 boy2 = Gender2.BOY; System.out.println(boy); System.out.println(boy2 == boy); 答案: BOY true 分析: 首先,枚举类本质也是类,所以Gender2 boy = Gender2.BOY;这种写法肯定是对的 boy相当于拿到了枚举类里的public static final常量,因此boy和boy2肯定是一样的 System.out.println(boy)相当于调用Gender2的toString方法,但是显然它没有,就去找父类的toString方法,父类是java.lang.Enum(前面有提到) Enum类的成员方法 分别是 name、ordinary、values、valueOf、compareTo方法,建议自己写一写用一用,方法的效果都写在代码里了: public class Enum { public static void main(String[] args) { Season spring = Season.SPRING; // name方法,建议优先使用toString,效果类似 System.out.println(spring.name()); System.out.println(spring); // ordinary方法,输出该枚举对象的序号,从0开始 System.out.println(spring.ordinal()); // values方法,返回所有的枚举对象 Season[] values = Season.values(); for (Season value : values) { System.out.println(value); } // valueOf方法,返回指定名称的枚举对象 Season valueOf = Season.valueOf("FALL"); System.out.println(valueOf); // compareTo方法,比较两个枚举对象,返回它们的序号之差,在这里是spring的序号 - valueOf的序号 System.out.println(spring.compareTo(valueOf)); } } enum Season{ SPRING("春天","温暖"), SUMMER("夏天","炎热"), FALL("秋天","凉爽"), WINTER("冬天","寒冷"); private String name; private String desc; Season(String name, String desc) { this.name = name; this.desc = desc; } } 简单练习2 声明 Week 枚举类,其中包含星期一至星期日的定义; MONDAY, TUESDAY, WEDNESDAY, THURSDAY,FRIDAY, SATURDAY, SUNDAY; 使用 values 返回所有的枚举数组,并遍历,要求打印值为“星期一”而不是“MONDAY” public class Enum { public static void main(String[] args) { Week[] values = Week.values(); for (Week value : values) { System.out.println(value); } } } enum Week{ MONDAY("星期一"), TUESDAY("星期二"), WEDNESDAY("星期三"), THURSDAY("星期四"), FRIDAY("星期五"), SATURDAY("星期六"), SUNDAY("星期日"); private String name; Week(String name) { this.name = name; } @Override public String toString() { return name; } Enum类的接口 Enum类本身已经有了继承关系,因此不能继承其他类 但作为一个类,它仍然可以实现接口 interface Playing { void play(); } enum Music implements Playing { HARD_ROCK, POP, CLASSIC, ROCK, JAZZ; @Override public void play() { } } 注解 最基本的修饰符 最基本的三个修饰符分别是: Override:用来限定某个方法必须重写父类的方法,只能用于方法 SuppressWarnings:抑制编译器的警告 Deprecated:用来表示某个程序元素(比如类或者方法)已经过时 Override 其实对于正确的方法重写来说,加不加这个修饰符都可以。 但如果加了的话,编译器会检查你是否有正确地重写这个方法。如果不正确的话会报错,产生编译错误。 Override的源码如下,从 @Target (ElementType.METHOD) 上可以看出,这个修饰符只能用在方法上。 @interface 修饰的类都是注解类。 @Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { } 顺带一提, @Target 是修饰注解的注解,也称为元注解。 Deprecated 用 @Deprecated 修饰符修饰的元素,暗示其已经过时了,不推荐再继续使用,但其实可以使用。 @Documented @Retention(RetentionPolicy.RUNTIME) @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE}) public @interface Deprecated { String since() default ""; boolean forRemoval() default false; } 从源码可以看出,该修饰符可以修饰方法、字段、包、参数等。 该关键字一般用于 JDK 版本更迭时,给过时的方法打上标注。 SuppressWarnings 用来抑制编译器警告,在 ( ) 中可以填抑制的警告类型。 因为懒得打字,我直接把文档粘在这: 参数名称 作用描述 all 抑制所有警告。 unchecked 抑制与泛型相关的“未经检查的操作”警告,例如在使用原始类型时。 deprecation 抑制使用了 @Deprecated 标记的过时类或方法的警告。 rawtypes 抑制使用了泛型但未指定具体类型的“原始类型”警告。 unused 抑制代码中存在但未被使用的变量、方法或类的警告。 serial 抑制可序列化的类未定义 serialVersionUID 的警告。 null 抑制与空值分析相关的警告(如潜在的空指针)。 cast 抑制与强制类型转换操作相关的警告。 fallthrough 抑制在 switch 语句中缺少 break 而导致“直通”的警告。 finally 抑制 finally 块无法正常返回的警告。 boxing 抑制与自动装箱(boxing)和拆箱(unboxing)操作相关的警告。 static-access 抑制不正确的静态成员访问方式的警告。 dep-ann 抑制使用过时注解的警告。 incomplete-switch 抑制 switch 语句中未覆盖所有枚举常量的警告。 javadoc 抑制与 Javadoc 相关的警告。 synthetic-access 抑制内部类访问未优化的警告。 resource 抑制与资源(如 Closeable )使用相关的警告。 restriction 抑制使用了不建议或禁止引用的警告。 使用示例,代码如下: @SuppressWarnings({"all"}) enum Music implements Playing { HARD_ROCK, POP, CLASSIC, ROCK, JAZZ; @Override public void play() { } } @SuppressWarnings是没有对使用位置限制的,从源码中也可以看出,它没有 @Target 去限制,源码如下: @Retention(RetentionPolicy.SOURCE) public @interface SuppressWarnings { String[] value(); } 元注解 注解的注解,看源码时可能遇到,没那么重要,快速过一下。 四种元注解: Retention:指定注解的作用范围,三种值SOURCE,CLASS,RUNTIME Target:指定注解可以在哪些地方使用 Documented:指定该注解是否会在javadoc体现,即在生成文档的时候,可以看到该注解 Inherited:子类会继承父类注解 这部分我战略性跳过了,稍微不太好理解,也有点深入了。 1 个帖子 - 1 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-10 15:41:57+08:00 · tech

背景:java全栈,codex+5.4或者claudeCode + deepseek/opus4.6,以及一个清澈愚蠢的我。 需求:我想要的把一整个模块都能丢给ai去自动完成,完成一个比较大的长时间任务 这些东西基本是增删改查,实际上人工拆任务拆下来都不难。但真正生成往往不让我满意,出现的结果包括但不限于: 没有完成明确写出的任务 写的东西莫名奇妙超出边界 功能不理解怎么做就全删了 自己尝试造早就实现的轮子(CLAUDE.md中甚至就写了有这个模块) 非常糟心。 我先后尝试过: superpowers:执行结果是最好的,但慢的说实话还不如我自己写 ECC:感觉什么东西都要审批,太麻烦了 Speckit:目前感觉最好用的,但是很多东西审查的挺累,也是最明显理解一点点跑歪掉的,而且这个写plan的能力感觉忽高忽低的 CLAUDE.md现在用的是那个karpathy的CLAUDE.md+项目技术栈+一些索引,说实话感觉用处好像也不是很大,有问题ai还是不会停下问问题 4 个帖子 - 4 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-10 15:07:28+08:00 · tech

本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 LINUX DO 社区: 是 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出 前言 本教程的环境基于 jdk8 + langchain4j 0.35 教程源码放在这里了: github.com GitHub - worenbudaoni/rag-study-helper: 一个学习检索增强生成的全流程助手 一个学习检索增强生成的全流程助手 文章内容 因为内容比较多,我会从下面三个文章进行讲解,后续发布后会贴出来,这节讲: RAG实现全流程 RAG实现全流程 接入飞书WIKI文档 接口限流:令牌桶 + AOP RAG实现全流程 RAG、Embeding、Reranker、向量、向量数据库 是什么 本文主要讲RAG流程+代码,所以在讲完之后我会把这些列出来完整讲一下,这里就用大白话一笔带过了 RAG (检索增强生成(Retrieval Augmented Generation))是一种技术,它先从一个知识库中检索出相关的信息片段,再把这些信息“喂”给大语言模型,让它基于这些事实来生成更准确的答案。 Embedding (文本嵌入模型)嵌入是将文本、图像等数据转换为固定长度的向量表示,使得语义相似的内容在向量空间中距离更近,是检索步骤的关键技术。 Reranker (重排序模型)是对初步检索结果进行二次精排,根据与查询的相关性重新打分排序,以提升最终召回结果的质量。 向量 是将文本、图像等数据转换为一串数值数组(如 [0.1, 0.5, -0.2, ...] ),用来表示其语义或特征。相似的内容在向量空间中距离更近。 向量数据库 是专门存储和检索向量的数据库,支持高效的相似性搜索(如余弦相似度、欧氏距离),常用于 RAG 等场景中快速找到最相关的内容。 RAG 通用实现思路(图文) 图(配合下文观看更佳) 文 入库流程:文件->分割器->Embedding->向量数据库(入库) 文件 :(word、pdf、ppt、md、excel、图片)通过各种手段(POI、OCR)转成 字符串 分割器 :一个文件可能有几万几十万的字符,我们提问的知识可能只是其中的一个片段,如果把整个文件转向量存储起来,通过我们的提问从向量数据库找到了这个几万字符的文档,把这几万的字符喂给LLM耗token不说,无关的知识可能混淆内容,为了 精简内容、提取精华 ,我们就要用到分割器,分割器的功能就是通过段落、句子,把一长段字符串拆解成 文本段 。分割器的实现方式有很多,没有那种是银弹,只有合适才最重要。 Embedding(文本嵌入模型) :这个也是属于语言模型的一种,但它不擅长生成文本,而擅长理解语义并把字符串(入库流程中就是把分割器分出来的多个字符串)转为 向量 向量数据库 :我们把 文本段 和 向量 (如果要做权限管控,支持添加Metadata)都存进向量数据库中(PS:后续的提问流程就是通过问题转成向量去向量数据库通过余弦相似度(后面我会讲)得到我们需要的文本段) 提问流程:问题->问题重写->Embedding->向量数据库(匹配出库)->相关性筛选->rerank->prompt优化(文本段+问题)->LLM回答 问题 :就是我们的提问 问题重写 :在 RAG 场景下,你通过 《如何学习JAVA》 这个文档去检索,你第一次问:“java 要学什么框架”,embedding 根据 “java 要学什么框架” 转成向量去向量数据库检索到了相应的内容再放进 prompt 喂给 LLM,LLM 根据文档说:“springboot”,你第二次问:“他有什么好处”,我们可以一眼就看出这里的他指的是 springboot ,但是 embedding 模型不知道,embedding 只是把你输入的 “他有什么好处” 转换成向量去向量数据库查询,所以查出来的根本就不是你想要的文档内容,这时 LLM 就不会根据文档去生成你想要的内容了,问题重写就是根据你的上下文让LLM重写你的问题,使 embedding 生成的向量更能在向量数据库检索到更准确的文档内容,当然每次提问都要重写肯定费时又费token,所以我们要判断什么情况下重写问题能得到更好的效果,我的重新逻辑就是提问小于5个字且包含他、她、它、上述等关键词时进行重写 Embedding(文本嵌入模型) :这个也是属于语言模型的一种,但它不擅长生成文本,而擅长理解语义并把字符串(提问流程中就是把问题)转为 向量 向量数据库 :我们根据 向量 (如果有权限管控,支持过滤Metadata)去数据库中找到对应的 文本段 (选取 top 20,当然多少都可以自定义) 相关性筛选 :人和香蕉的DNA都有50%以上的相似度,所以我们要筛选掉相关的数据库,余弦相似度的取值范围是 [-1, 1], 越接近 1 相似度越高 ,反之越低,这里我们就要设置一个阈值(我设置的为0.77),抛弃掉相似度低的数据 rerank :就是把你的 问题 和从向量数据库得到的 文本段 对比,把最先关的文档排前面(检索召回 top 20,但真正有价值的可能只有其中 3-5 条,通过 rerank 可以让上下文质量更高,回答更准,还省 token) prompt优化(文本段+问题) :这里就是根据你的需求优化关键词了,比如你是金融公司要精切的答案,就prompt添加 “参考文档(文本段) 严格基于参考文档回答,不要使用你自己的知识 回答下面问题(问题)” 等等 LLM回答 :目前市面上的AI相关功能,什么Agent、CLI、Cursor等等都是基于LLM来实现的,RAG也不例外 RAG 代码实践 代码已经全部开源,并且代码里面有很多的注解,这里就讲重要的代码片段 一、配置 LangChain4j + Embedding 模型 + 向量数据库:LangChain4jConfig.java LangChain4j、Embedding :选用支持 Openai API 的模型,直接替换配置就可以了 Reranker :重定向这个模型,langchain没有支持 Openai API ,所以后续我们根据模型平台的接口文档去手搓一个使用,当然 LangChain4j、Embedding 这些都能手搓,但别人已经把轮子创建好了,就不要再重复造了 向量数据库 :这里我放了三套配置供大家筛选,只要合适自己的需求就好,没必要什么最好就上什么,成本摆在那的,但生产不要用 InMemory ,就丢失数据这一条就是不能接受的 1、 InMemory :纯内存单机库,好处是不依赖第三方组件,坏处是程序退出即丢失数据(生产不要用) 2、 Chroma :嵌入式本地库,零配置,部署极简,支持百万级别向量存储,单节点架构 3、 Milvus :分布式云原生库,高并发、低延迟,支持十亿级向量,分布式架构,支持水平扩展与分片 /** * LangChain4j 配置 */ @Configuration public class LangChain4jConfig { @Value("${langchain4j.open-ai.chat-model.api-key}") private String chatApiKey; @Value("${langchain4j.open-ai.chat-model.base-url}") private String chatBaseUrl; @Value("${langchain4j.open-ai.chat-model.model-name}") private String chatModelName; @Value("${langchain4j.open-ai.chat-model.temperature}") private Double temperature; @Value("${langchain4j.open-ai.embedding-model.api-key}") private String embeddingApiKey; @Value("${langchain4j.open-ai.embedding-model.base-url}") private String embeddingBaseUrl; @Value("${langchain4j.open-ai.embedding-model.model-name}") private String embeddingModelName; // ── InMemory (可以自己玩,生产不要用) ── // 没有配置的时候默认使用 InMemory 但生产环境不建议用这个配置,可以删掉,开发环境可以自己玩 @Bean @ConditionalOnProperty(name = "vector.store.type", havingValue = "in-memory", matchIfMissing = true) public EmbeddingStore<TextSegment> inMemoryEmbeddingStore() { return new InMemoryEmbeddingStore<>(); } // ── Chroma (中小型项目首选) ── @Value("${chroma.host:localhost}") private String chromaHost; @Value("${chroma.port:8000}") private Integer chromaPort; @Value("${chroma.collection-name:rag_study_helper}") private String chromaCollectionName; @Bean @ConditionalOnProperty(name = "vector.store.type", havingValue = "chroma") @Lazy public EmbeddingStore<TextSegment> chromaEmbeddingStore() { return ChromaEmbeddingStore.builder() .baseUrl("http://" + chromaHost + ":" + chromaPort) .collectionName(chromaCollectionName) .build(); } // ── Milvus (大型项目首选) ── @Value("${milvus.host:localhost}") private String milvusHost; @Value("${milvus.port:19530}") private Integer milvusPort; @Value("${milvus.collection-name:rag_study_helper}") private String milvusCollectionName; @Value("${milvus.dimension:2048}") private Integer milvusDimension; @Bean @ConditionalOnProperty(name = "vector.store.type", havingValue = "milvus") @Lazy public MilvusEmbeddingStore milvusEmbeddingStore() { return MilvusEmbeddingStore.builder() .host(milvusHost) .port(milvusPort) .collectionName(milvusCollectionName) .dimension(milvusDimension) .build(); } // LLM模型选择(要选择适配 OpenAI API 的模型) @Bean public OpenAiChatModel chatModel() { return OpenAiChatModel.builder() .apiKey(chatApiKey) .baseUrl(chatBaseUrl) .modelName(chatModelName) .temperature(temperature) .timeout(Duration.ofSeconds(60)) // 本地的计数器,用来知道当前对话有多长,跟模型实际输出无关 .tokenizer(new OpenAiTokenizer()) .build(); } // LLM流式模型(要选择适配 OpenAI API 的模型) @Bean public OpenAiStreamingChatModel streamingChatModel() { return OpenAiStreamingChatModel.builder() .apiKey(chatApiKey) .baseUrl(chatBaseUrl) .modelName(chatModelName) .temperature(temperature) .timeout(Duration.ofSeconds(60)) .tokenizer(new OpenAiTokenizer()) .build(); } // 向量嵌入模型(要选择适配 OpenAI API 的模型) @Bean public OpenAiEmbeddingModel embeddingModel() { return OpenAiEmbeddingModel.builder() .apiKey(embeddingApiKey) .baseUrl(embeddingBaseUrl) .modelName(embeddingModelName) .timeout(Duration.ofSeconds(60)) .build(); } } 二、入库流程:文件解析->分割器->Embedding->向量数据库(入库) 1、文件判重 + 解析文件 + 入库: DocumentIngestionService.java public DocumentInfo ingestDocument(String fileName, InputStream inputStream) throws IOException { // 通过 文件流 + hash 来判断文件是否重复 byte[] content = IOUtils.toByteArray(inputStream); String hash = sha256(content); log.info("Ingesting document: {}, hash={}", fileName, hash); // 检查文件是否已经入库 Documents existing = documentsMapper.selectOne( Wrappers.<Documents>lambdaQuery().eq(Documents::getContentHash, hash) ); // 文档存在就返回 if (existing != null) { log.info("Document already ingested: {} (hash={})", existing.getDocumentName(), hash); // 这个类不用管,给前端返回的实体类而已,不重要 return new DocumentInfo(existing.getId(), existing.getDocumentName(), existing.getChunkCount()); } // 解析文档 把文件转为 Document 类(LangChain4j 包下面的文档类) // parseDocument 个是通用方法,根据文件后缀去解析,下面的 parseWord 方法就是 parseDocument 方法解析到 .docx 文件后进行解析,就看下面的 parseWord 方法即可,不看也行 Document document = parseDocument(fileName, new ByteArrayInputStream(content)); // 入库(向量数据库 + 关系型数据库) return processAndSave(document, fileName, "UPLOAD", hash, (long) content.length, null, null, null, "upload"); } 2、文件解析: DocumentIngestionService.java 这个不用过多关注,网上解析文档一搜一大堆 private Document parseWord(InputStream inputStream) throws IOException { StringBuilder text = new StringBuilder(); try (XWPFDocument doc = new XWPFDocument(inputStream)) { for (XWPFParagraph para : doc.getParagraphs()) { text.append(para.getText()).append("\n"); } for (XWPFTable table : doc.getTables()) { for (XWPFTableRow row : table.getRows()) { for (XWPFTableCell cell : row.getTableCells()) { text.append(cell.getText()).append(" | "); } text.append("\n"); } text.append("\n"); } } return Document.from(text.toString()); } 3、入库:分割器、Embedding、向量数据库: DocumentIngestionService.java private DocumentInfo processAndSave(Document document, String fileName, String source, String contentHash, Long fileSize, String feishuNodeToken, String feishuObjType, Long feishuUpdateTime, String creator) throws IOException { // 按 token 分割 OpenAiChatModelName.GPT_3_5_TURBO 为默认值,不加这个也行,这里写进入只是展示 Tokenizer tokenizer = new OpenAiTokenizer(OpenAiChatModelName.GPT_3_5_TURBO); String prefix = "[来源:" + fileName + "]\n"; // 计算文件名占多少token,后面会讲为什么需要计算 int prefixTokenCount = tokenizer.estimateTokenCountInText(prefix); // 占用token比较高的文件名就记一下日志 if (prefixTokenCount > 200) { log.warn("文件名前缀占用 token 过多: {} tokens, fileName={}", prefixTokenCount, fileName); } // 我用的 Embeding 模型(bge-large-zh-v1.5)解析单条字符串的上限为 512 token (虽然 bge-large-zh-v1.5 上限低,但他在硅基流动上是免费的,而且能力也不错) // 如果切换模型后,那么向量数据库中记录的数据都不可用了,要注意哦 int maxSegmentSize = Math.max(50, 512 - prefixTokenCount); // 段落间重叠token数 int maxOverlap = 51; // 基于 token 的分割器,层级降级(官方推荐) // 整体分段 token 数 <= 512,段落间重叠token数 占整体的 10%-20% ,就是说文本段最少占 380 的 token 量(文件名比较小的情况下),就是说一个分片差不多 250 ~ 300 个汉字 DocumentSplitter splitter = DocumentSplitters.recursive( // maxSegmentSize: 每个分段最大token数 maxSegmentSize, // maxOverlap: 段落间重叠token数 maxOverlap, // separator 优先级 tokenizer ); // 分割器分隔文档 List<TextSegment> segments = splitter.split(document); // 给文本段添加 文件名 前缀 segments.replaceAll(textSegment -> TextSegment.from( prefix + textSegment.text())); List<Embedding> allEmbeddings = new ArrayList<>(); // 记录嵌入成功的文本段,保证与 allEmbeddings 一一对应,避免失败时错位 List<TextSegment> successSegments = new ArrayList<>(); // 一次 http 请求 10 条,避免反复建立连接增大开销 int batchSize = 10; for (int i = 0; i < segments.size(); i += batchSize) { int end = Math.min(i + batchSize, segments.size()); List<TextSegment> batch = segments.subList(i, end); try { List<Embedding> embeddings = embeddingModel.embedAll(batch).content(); allEmbeddings.addAll(embeddings); successSegments.addAll(batch); log.info(" Embedded batch {}-{}/{}", i, end, segments.size()); } catch (Exception e) { log.warn(" Batch {}-{} failed, trying one-by-one", i, end); for (TextSegment seg : batch) { try { allEmbeddings.add(embeddingModel.embed(seg.text()).content()); successSegments.add(seg); } catch (Exception e2) { log.warn(" Skipping chunk: {}", seg.text().substring(0, Math.min(50, seg.text().length()))); } } } } // 向量数据库添加数据,返回 向量 ID 用于关系型数据库保存 List<String> vectorIds = embeddingStore.addAll(allEmbeddings, successSegments); // 文档类型 String docType = "unknown"; int dotIdx = fileName.lastIndexOf('.'); if (dotIdx > 0) { docType = fileName.substring(dotIdx + 1).toLowerCase(); } // 文档入库 Documents docRecord = new Documents(); docRecord.setDocumentName(fileName); docRecord.setDocumentType(docType); docRecord.setSource(source); docRecord.setContentHash(contentHash); docRecord.setFileSize(fileSize != null ? fileSize : 0L); docRecord.setChunkCount(successSegments.size()); docRecord.setFeishuNodeToken(feishuNodeToken); docRecord.setFeishuObjType(feishuObjType); docRecord.setFeishuUpdateTime(feishuUpdateTime); docRecord.setCreator(creator); documentsMapper.insert(docRecord); // 向量分片入库 for (int i = 0; i < successSegments.size(); i++) { DocumentChunks chunk = new DocumentChunks(); chunk.setDocumentId(docRecord.getId()); chunk.setVectorId(vectorIds.get(i)); chunk.setChunkIndex(i); chunk.setChunkText(successSegments.get(i).text()); documentChunksMapper.insert(chunk); } log.info("Ingested {} with {} chunks, documentId={}", fileName, successSegments.size(), docRecord.getId()); return new DocumentInfo(docRecord.getId(), fileName, successSegments.size()); } 三、提问流程:问题->问题重写->Embedding->向量数据库(匹配出库)->相关性筛选->rerank->prompt优化(文本段+问题)->LLM回答 1、 RagQueryService.java 整个提问的主流程,我浓缩进了一个方法,除了 rerank 的实现(也会贴出来),可以直接打开我的项目在根据下文的提示来追踪,项目内没有文章详细 // sessionId:会话 ID,用来查找上下文,question 用户的问题 // callback:控制层返回也是用的 SSE 格式,所以需要通过 langchain4j 的匿名内部类来传消息 public void streamAnswer(String sessionId, String question, StreamingResponseHandler<AiMessage> callback) { // 获取历史上下文 List<ChatMessage> history = conversationStore.getHistory(sessionId); // 提问小于 5 个字,进行问题重写,为了使 RAG 检索更准确 // 比如 RAG 场景下 你通过 《如何学习JAVA》 这个文档去检索(如果不了解向量数据库和embedding就先别管,只看问题) // 你第一次问:"java 要学什么框架" // embedding 根据 "java 要学什么框架" 转成向量去检索到了相应的内容再放进 prompt 喂给 LLM // LLM 根据文档说:"springboot" // 你第二次问:"他有什么好处" // 我们可以一眼就看出这里的他指的是 springboot ,但是 embedding 模型不知道 // embedding 只是把你输入的 "他有什么好处" 转换成向量去向量数据库查询,所以查出来的根本就不是你想要的文档内容 // 这时 LLM 就不会根据文档去生成你想要的内容了 String searchQuery = queryRewriteService.rewrite(question, history, 5); if (!searchQuery.equals(question)) { log.info("Search query rewritten: \"{}\" → \"{}\"", question, searchQuery); } // embedding(向量嵌入模型)根据你的问题转换成向量 Embedding questionEmbedding = embeddingModel.embed(searchQuery).content(); // 查询 向量数据库 找出前 20 个最相似的向量 EmbeddingSearchRequest searchRequest = EmbeddingSearchRequest.builder() .queryEmbedding(questionEmbedding) .maxResults(20) .build(); EmbeddingSearchResult<TextSegment> searchResult = embeddingStore.search(searchRequest); // 查出来的向量结果 List<EmbeddingMatch<TextSegment>> matches = searchResult.matches(); for (EmbeddingMatch<TextSegment> m : matches) { log.info(" score={} content={}", m.score(), m.embedded().text().substring(0, Math.min(80, m.embedded().text().length()))); } if (!matches.isEmpty()) { DoubleSummaryStatistics stats = matches.stream() .mapToDouble(EmbeddingMatch::score).summaryStatistics(); long above90 = matches.stream().filter(m -> m.score() >= 0.90).count(); long above80 = matches.stream().filter(m -> m.score() >= 0.80 && m.score() < 0.90).count(); long above70 = matches.stream().filter(m -> m.score() >= 0.70 && m.score() < 0.80).count(); long below70 = matches.stream().filter(m -> m.score() < 0.70).count(); log.info("Score distribution: max={} min={} avg={} | ≥0.90={} 0.80-0.89={} 0.70-0.79={} <0.70={}", stats.getMax(), stats.getMin(), stats.getAverage(), above90, above80, above70, below70); } // 这里的效果 = searchRequest.minScore(scoreThreshold) 但这个阈值跟模型文档问题都有关系,没有什么标准的值,所以为了方便后续调试和检验适合自己项目的阈值,这里有必要打印一下日志,并且追踪日志进行观测 // 设置一个阈值,低于这个阈值的向量被过滤掉 // 就是说你问 "java 是什么" 检索结果里是 "怎么做红烧肉" 这种跟 question 余弦相似度很低的 那么把这个检索丢给 LLM 有什么用呢 double threshold = scoreThreshold; List<TextSegment> relevant = matches.stream() .filter(m -> m.score() >= threshold) .map(EmbeddingMatch::embedded) .collect(Collectors.toList()); log.info("After filtering question={} score>={} chunks={}", question, threshold, relevant.size()); // 检索召回 top 20,但真正有价值的可能只有其中 3-5 条,rerank 就是把最有用的排到最前面 // 通过 rerank 找到最相关的 5 条(这里的 5 可以自定义) // 效果就是喂给 LLM 的上下文质量更高,回答更准,还省 token if (!relevant.isEmpty()) { relevant = rerankService.rerank(searchQuery, relevant, 5); } // 自定义 prompt 模板,如果检索结果为空,则使用普通对话模式 String prompt; if (relevant.isEmpty()) { log.info("No relevant docs found, using normal chat mode"); prompt = "你是一个智能助手。请回答用户的问题。\n\n" + "## 问题\n" + question; } else { String context = relevant.stream() .map(TextSegment::text) .collect(Collectors.joining("\n\n---\n\n")); prompt = "## 角色\n" + "你是一个基于内部文档的数据分析助手。\n\n" + "## 参考文档\n" + context + "\n\n" + "## 约束\n" + "- 回答时请标注信息来源,格式:根据 [来源:文件名] 的记载/显示...\n" + "- 严格基于参考文档回答,不要使用你自己的知识\n" + "- 如果参考文档中没有相关信息:\n" + " - 完全不相关:回复\"根据文档内容,没有找到相关信息\"\n" + " - 部分相关:说明文档中涉及了什么,明确指出未涉及的部分\n" + "- 回答时引用具体的行/数据来支撑你的结论\n" + "- 用中文回答\n\n" + "## 问题\n" + question; } // 使用 langChain 调用适配 OpenAI API 的模型生成答案 StringBuilder fullAnswer = new StringBuilder(); streamingChatModel.generate(prompt, new StreamingResponseHandler<AiMessage>() { @Override public void onNext(String token) { fullAnswer.append(token); callback.onNext(token); } @Override public void onComplete(Response<AiMessage> response) { TokenUsage usage = response.tokenUsage(); if (usage != null) { log.info("Token 用量 - 输入: {}, 输出: {}, 总和: {}", usage.inputTokenCount(), usage.outputTokenCount(), usage.totalTokenCount()); } // 储存上下文 conversationStore.addTurn(sessionId, question, fullAnswer.toString()); callback.onComplete(response); } @Override public void onError(Throwable error) { callback.onError(error); } }); } 2、 RerankService.java 这个也没什么讲的,只是按照官方api请求并获取罢了: 创建重排序请求 /** * 重排序 * 就是把你查的内容和从向量数据库得到的文档分片对比,把最先关的文档排前面 */ public List<TextSegment> rerank(String query, List<TextSegment> documents, int topN) { if (documents.isEmpty()) { return documents; } Map<String, Object> requestBody = new HashMap<>(); requestBody.put("model", modelName); requestBody.put("query", query); requestBody.put("documents", documents.stream() .map(TextSegment::text) .collect(Collectors.toList())); requestBody.put("top_n", topN); MediaType JSON = MediaType.parse("application/json; charset=utf-8"); try { String json = objectMapper.writeValueAsString(requestBody); String url = baseUrl + "/rerank"; Request request = new Request.Builder() .url(url) .post(RequestBody.create(json, JSON)) .header("Authorization", "Bearer " + apiKey) .build(); log.info("Calling SiliconFlow Rerank: {} documents, query=\"{}\"", documents.size(), truncate(query, 50)); try (Response response = httpClient.newCall(request).execute()) { String body = response.body() != null ? response.body().string() : ""; if (response.isSuccessful() && !body.isEmpty()) { return parseAndReorder(body, documents); } } } catch (Exception e) { log.warn("Rerank API call failed, falling back to original order: {}", e.getMessage()); } return documents; } 测试 RAG 项目 java环境:只需要填入 app.rag.chat-api-key 和 app.rag.embedding-api-key 即可运行 docker环境: .env.example 文件去掉 .example 填入 APP_RAG_CHAT_API_KEY 和 APP_RAG_EMBEDDING_API_KEY 运行命令 docker compose up -d 即可,要使用 Chroma 或者 Milvus 请运行对应的 docker compose 文件 一、乱生成一个不存在的文档 二、上传文档走入库流程 三、提问 四、测试完成,撒花 向量、模型、分割器选择 大语言模型的选择支持 OpenAI API 的模型就行了,项目中用的就是 deepseek 官方的模型,关于 LLM 的选择区别我就不讲了,什么Qwen、MIMO、MiniMax、GLM啥的都可以 一、向量数据库 我的项目实现了前三个数据库的配置,需要注意的是使用Milvus的时候需要填写向量维度 milvus.dimension: 1024 这个值需要和 embeding 嵌入模型 的维度所匹配(维度的意思下文介绍 embeding 模型选择 有写) 数据库 类型 适合场景 LangChain4j In-Memory 索引库 / 内存库 Java 技术栈的 RAG 原型验证;中小型项目中追求极致低延迟的检索场景。(生产不要用) Chroma 开源 个人开发者、原型验证(PoC)、小型项目 Milvus 开源 超大规模、企业级生产系统;有专业运维团队 Pinecone 商业 追求极简运维、快速上线的商业项目 Qdrant 开源 高并发、低延迟检索;自托管且注重性价比 Weaviate 开源 知识库、智能问答等需结合语义与结构化关系 Pgvector 扩展 数据已在PG的轻量级RAG或MVP项目 Elasticsearch 混合引擎 已有ES技术栈,需要“文本+向量”一体化搜索 腾讯云VectorDB 商业 企业级RAG、智能客服;尤其适合腾讯云生态的企业 二、Embedding(文本嵌入模型) 维度 :根据输入得到的信息密度,比如(只是个例子,不是真实模型生成的):“我是MT” 转成 1维 的向量就是 [0.233] 二维就是 [0.520,0.666] 通常维度越高,信息容量越大,查找的内容就更精确 最大上下文 :就是把文本处理成向量最大的 token 数量,超出后一般操作就是截断,造成的结构就是你文章 800 字 最大上下文 500 字,截取了前 500 字转成向量,前面500字没有 java 相关内容,但后面 300 字有,这样你查 java 相关内容时,就截取不到这个分片了(上下文是指的字符转成的token数,这里为了方便大家理解我换成汉字来解说) 我项目用的是 BAAI/bge-large-zh-v1.5 1024维 512最大token ,嵌入模型不能随意切换,切换模型后向量输出都不一样了,那么从向量数据库查的东西都是乱的,数据就报废了 模型 厂商 维度 最大上下文 中文语义能力 部署成本 适用场景 Youtu-Embedding 腾讯优图 ~2048 ~512-8k C-MTEB榜首 (77.46) 开源 , 2B参数 企业级RAG,高精度通用任务 Qwen3-Embedding (2B/8B) 阿里Qwen 2048 ~8k-32k 专为表征设计,同尺寸SOTA 开源 先进LLM-Backbone向量化 BAAI/bge-large-zh-v1.5 智源研究院 1024 512 tokens C-MTEB榜首,中文RAG领先 开源免费 通用中文语义搜索/问答 GLM-Embedding 智谱AI 1024 8k tokens 中文RAG召回率领先 (83.5%) 商业API (~0.5元/1M tokens) 商业应用,极致中文精度 Conan-Embedding-V2 腾讯 ~1536 32k tokens MTEB中英SOTA,支持跨语言检索 开源 , 1.4B参数 长文档处理,中英混合场景 BAAI/bge-small-zh-v1.5 智源研究院 512 512 tokens 轻量高效 开源免费 边缘设备,对延迟敏感的应用 M3E-large Moka AI 1024 512 tokens 中文社区积累深厚 开源免费 社区支持好,快速原型验证 text-embedding-3-large OpenAI 3072 8k tokens 对非英文内容表现一般 商业API (~$0.13/1M tokens) 国际化应用,生态完善 三、Reranker(重排序模型) 我项目用的是 BAAI/bge-reranker-v2-m3 这个其实没啥好讲的,就是把问题和文本切片让模型排个序,随时都可以切换模型,需要注意的就是这个最大上下文,但一般来说重排序的模型都比嵌入模型大 模型 最大上下文 开发方 / 类型 适合场景 BAAI/bge-reranker-v2-m3 8192 tokens 智源研究院 / 开源 各类RAG与搜索场景的“万金油”首选,尤其是中文环境和需要自托管、注重性价比和响应速度的应用 BAAI/bge-reranker-v2-m3-4B 8192 tokens 智源研究院 / 开源 对排序精度有极致要求,且具备较强算力资源的专业RAG系统 Qwen3-Reranker-8B 32768 tokens 阿里通义千问 / 开源 多语言、跨语种或代码检索,预算充足、追求世界级顶级精度的应用 Cohere Rerank (v4) 32768 tokens Cohere / 商业API 追求极致便捷性和性能,不想投入运维精力的团队或有明确预算的商业项目 Jina Reranker v3 131072 tokens Jina AI / 开源(非商业) 需要处理超长文档(如整本书、长对话)的非商业研究或多模态RAG原型 Voyage rerank-2.5 32768 tokens Voyage AI / 商业API 企业级高精度搜索,特别是对召回质量要求苛刻、且预算充足的项目 mxbai-rerank-large-v2 未明确(通常≥512) mixedbread-ai / 开源+API 注重开源商业许可的稳定性,希望保留自托管或API两种灵活方案的项目 Zerank 2 8192 tokens (推测) Agentset / 商业API 毫秒级响应场景(如实时聊天检索),且能接受牺牲一点精度 MiniLM / DistilBERT-based 512 tokens (典型) 微软等 / 开源 资源极度受限的环境,如边缘设备或小型服务器 四、分割器(Document Splitters) 分割器这个就用我项目里的就行了,参数可以自己调一调 分割器 核心思路 主要适用场景 DocumentSplitters.recursive (python中是RecursiveCharacterTextSplitter,这两个是一样的) 递归尝试不同分隔符 (段落→换行→句子→单词→字符),直到块大小符合要求 。 通用首选 。TXT/Word/PDF等 任意格式文本的首选 。 DocumentByParagraphSplitter 按 段落 切分(连续两个或以上换行符 \n\n ) 。 网页、博客、Markdown格式规整,结构清晰的内容。 DocumentBySentenceSplitter 利用NLP库检测 句子边界 。 新闻、小说、聊天记录等自然语言文本,对语义连贯性要求高的任务。 DocumentByWordSplitter 按 单词 切分(至少一个空格 " " ) 。 英文文本、数据清洗、长单词序列。 DocumentByCharacterSplitter 按 字符 暴力切分 。 作为其他分割器的 底层保底策略 ;资源极度受限的环境。 DocumentByLineSplitter 按 换行符 \n 切分 。 日志文件、CSV数据、代码行等每行独立成块的内容。 DocumentByRegexSplitter 按 自定义正则表达式 切分 。 按特定模式(如日期、章节号、XML标签、JSON块等)分割的复杂文档。 后话 我做这个项目的目的是为了方便后续再有 RAG 的需求时可以直接复用代码,所以我做了一个通用的企业级 RAG 的案例,但既然是通用的,所以很多需要定制化的需求我没有加上,看完整篇文章后,需要定制化需求和细节优化,可以看这里 一、Embedding模型选择:选择维数多且支持最大上下文大的模型 二、Reranker模型选择:选择支持最大上下文大的模型 三、权限管控:在项目中搜 todo 权限 有注释的代码用 1、使用RBAC模型,文档分配给角色,角色分配给用户 2、入库流程:公用文档存入 Metadata 字段 public ,私有文档 Metadata 字段添加关系型数据库的文档 ID 3、提问流程:用户搜索时查询到自己权限的文档 ID,查询向量数据库时添加 Metadata 过滤字段(public、文档) 四、向量数据库Milvus: 1、注意dimension字段要和embeding维度对应 2、可以使用混合搜索,通过 稠密向量 + 稀疏向量 进行匹配得到结果更精准 五、上下文入关系型库: 1、redis过期事件监听(整条存,由于我的项目redis存上下文只存最新的10条对话,所以如果要用这个方案得自己改一下) 2、每次LLM生成完之后异步添加(单条存,通过sessionId+userId查询)(推荐) 六、文件解析:通过 OCR 将扫描版 PDF 转为文本(图片同理) 10 个帖子 - 10 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-10 14:36:06+08:00 · tech

27届双非 Java 外包实习要不要跑路?求建议 本人 27 届双非科班,Java 后端方向。 半个月前拿到一个小厂日常实习,入职后才知道是外包,公司业务主要是给银行、医院做仓储管理的,工资 3.5k/月。入职第二天就开始派活干。 目前比较难受的几个点: 项目组后端只有一个 女组长 和两个实习生,公司没有任何产品文档和接口文档,开发流程很不规范,需求都是靠她口头说,但是她的表达又很抽象,每次理解需求都非常痛苦。 项目内容基本都是 CRUD,比较担心外包实习经历后续在简历上可能也不太好包装。 每天通勤往返 两个多小时 ,回宿舍后基本也没精力再做其他项目。 现在正在纠结要不要跑路,但又苦于还没找到下一家,恳请各位佬友给点建议。 11 个帖子 - 8 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-09 15:49:25+08:00 · tech

我的其他笔记可以查看 JAVA学习记录总贴 内部类 基础知识 内部类的类的第五大成员。 五大成员分别是:属性、方法、构造器、代码块、内部类。 定义:一个类的内部又完整的嵌套了另一个类结构。被嵌套的类称为“内部类”,在外面的类称为“外部类”,内部类最大的特点就是可以访问私有属性,并且可以体现类与类之间的包含关系。 语法 class Outer{//外部类 class Inter{//内部类 } } class OtherOuter{//外部其他类 } PS:内部类是OOP的重难点,底层源码有大量的内部类,必须要要下来这一块。 分类 定义在外部类局部位置(比如方法内) 局部内部类(有类名) 匿名内部类(无类名,重点!!!!!!!) 定义在外部类的成员位置上 成员内部类(没用static) 静态内部类(使用static) class OuterClass { //成员内部类 class MemberInnerClass {} //静态内部类 static class StaticInnerClass{} //外部方法 public void outerMethod() { //局部内部类 class LocalInnerClass {} ​ //匿名内部类 Runnable runnable = new Runnable() { @Override public void run() { System.out.println("匿名内部类"); } }; runnable.run(); } } 局部内部类 细节 局部内部类定义在外部类的局部位置,比如在方法中,并且有类名。(代码块中也行,但罕见) 可以直接访问外部类的所有成员,包含私有的 不能添加访问修饰符,因为局部内部类本质上就是一个局部变量,局部变量不能使用修饰符,同理它也不能,但它可以使用final,因为局部变量也可以用final,顺带一提abstract也可以。 局部内部类是可以被继承的。 作用域:仅仅作用在定义它的方法或代码块中 局部内部类想访问外部类的成员,直接访问即可 外部类想访问局部内部类的成员,可以在作用域内实例化局部内部类,但是注意,必须在定义它之后再new 外部其他类不可能访问局部内部类,这个挺好理解,因为局部内部类本质局部变量,不在它作用域内。 如果外部类和局部内部类变量重名,则会遵循就近原则,优先访问到局部内部类的变量,如果想要访问外部类的成员,则用如下语法 外部类类名.this.成员名 ,ps:顺带一提,如果你不嫌脱裤子放屁,其实 外部类类名.this.成员名 这种语法,可以在类的所有地方精确调用到本类成员 再顺带一提,其实所有内部类都可以在内部继续写内部类,这也是它复杂的原因 class OuterClass { private int n1 = 10; private void m2() {} ​ public void m1() { class InnerClass { public void show() { // 可以直接访问外部类的所有成员,包括私有类型,包括方法 System.out.println("n1 = " + n1); m2(); } } class A extends InnerClass{ } InnerClass innerClass = new InnerClass(); } } 建议对着上面的代码,每一条细节都去自己实践一遍,看看违反了会报什么错。 匿名内部类 重点中的重点,这部分所有代码建议自己真的看完书去手敲一下,不要指望看一遍能懂,更不要单纯相信遇到的时候让AI来解释就好,没那么轻松。 特点 本质是类,底层会有独立的class字节码文件 属于内部类,定义在外部类/代码块,这类局部位置中 没有类名,其实底层是有的,但是程序员不关心,因而匿名 同时也是一个对象,类定义好的同时就已经被创建了 基本语法 new 父类/接口名(构造参数列表){ // 类体:重写父类/实现接口的抽象方法,也可以新增自定义成员 }; 基于接口的匿名内部类 class Outer04 { private int n1 = 10; ​ public void method() { // 基于接口的匿名内部类 // 1. 需求:使用IA接口,并创建对象 // 2. 传统方法是写一个类,实现接口,创建对象,并调用方法 // 3. 但如果我们的需求是,这个类只用一次,那么定义出来就有些浪费了,所以我们可以使用匿名内部类来简化开发 // inner的编译类型是IA,而inner的运行类型是匿名内部类 /*其实这里的底层含义是 class Outer04$1 implements IA { @Override public void cry() { System.out.println("匿名内部类实现了cry方法"); } } */ // 4. jdk底层创建了匿名内部类Outer04$1,然后创建了实例,并且把地址返回给inner // 5. 匿名内部类只能使用一次,不能再次使用 IA inner = new IA(){ @Override public void cry() { System.out.println("匿名内部类实现了cry方法"); } }; inner.cry(); System.out.println(inner.getClass()); IA inner1 = new IA(){ @Override public void cry() { System.out.println("匿名内部类实现了cry方法"); } }; System.out.println(inner1.getClass()); } } ​ interface IA { public void cry(); } 值得说的细节在注释里都已经写明了,我在此补充一点 Outer04$1 就是 JVM 自动为第一个匿名内部类分配的名字,数字表示该类在外部类中出现的顺序。 基于类的匿名内部类 class Outer04 { private int n1 = 10; public void method() { Father fa = new Father("张三"); Father fa1 = new Father("张三"){}; // 证明匿名类创建的不是Father类,而是匿名内部类 System.out.println(fa.getClass()); System.out.println(fa1.getClass()); Father fa2 = new Father("张三"){ @Override public void show() { System.out.println("匿名内部类重写show()"); } }; fa2.show(); ​ Animal animal = new Animal() { @Override public void eat() { System.out.println("匿名内部类重写eat()"); } }; animal.eat(); } } ​ class Father { private String name; public Father(String name) { this.name = name; } public void show() { System.out.println("show()"); } } ​ abstract class Animal{ public abstract void eat(); } 简单总结一些要点 匿名内部类可以重写方法 父类的构造器如果有参数,则你也要提供对应参数(如果有多个构造器,那你就提供符合其中一个的就行) 基于抽象类(接口)的匿名内部类,必须实现其中所有抽象方法 一些细节 匿名内部类,既有定义一个类的特性,也有创建对象的特性,是一个凉面派 可以直接访问外部类的所有成员,包括外部私有成员 不能添加访问修饰符(直觉看上去也没办法提交orz),因为它本质是局部变量 作用域仅在定义它的方法or代码块中 外部其他类也不能访问匿名内部类,因为其本质局部变量 如果外部类和匿名内部类变量重名,则会遵循就近原则,优先访问到局部内部类的变量,如果想要访问外部类的成员,则用如下语法 外部类类名.this.成员名 匿名内部类练习 题目1:写一个基于接口的匿名内部类,并把它作为方法参数传入 public class AnonymousClass { public static void main(String[] args) { // 方式1:直接在参数位置编写匿名内部类 f1(new AA() { @Override public void show() { System.out.println("匿名类实现接口"); } }); // 方式2:先创建匿名内部类对象,再传入参数 AA aa = new AA() { @Override public void show() { System.out.println("匿名类实现接口"); } }; f1(aa); } public static void f1(AA aa){ aa.show(); } } ​ interface AA{ void show(); } 题目2: 定义一个铃声接口 Bell,接口中包含 ring() 方法。 定义一个手机类 Cellphone,类中包含闹钟功能方法 alarmclock(Bell bell),方法的参数类型为 Bell。 测试手机类的闹钟功能:通过匿名内部类创建 Bell 接口的实现对象,作为参数传入 alarmclock 方法,调用 ring() 方法时打印:懒猪起床了。 再传入另一个匿名内部类对象,调用 ring() 方法时打印:小伙伴上课了。 public class AnonymousClass { public static void main(String[] args) { new Cellphone().alarmclock(new Bell() { @Override public void ring() { System.out.println("懒猪起床了"); } }); new Cellphone().alarmclock(new Bell() { @Override public void ring() { System.out.println("小伙伴上课了"); } }); } } interface Bell { void ring(); } class Cellphone { public void alarmclock(Bell bell){ bell.ring(); } } 重点:一定要写一写感受一下,这部分涉及:多态、继承、动态绑定、内部类,多个知识点混杂在一起,需要认真练习 成员内部类 成员内部类定义在外部类的成员位置,并且没有static修饰 可以访问外部类的所有成员 可以添加任意的修饰符,因为其地位相当于一个成员 成员内部类的作用域和外部类的其他成员一样,都是整个类体。 成员内部类可以调用外部类的所有成员,包括私有。 外部类可以访问成员内部类的所有成员,包括私有成员,不过,必须要创建实例才能访问。 外部其他类想要访问成员内部类,有两种方法,分别标注在代码里了 如果外部类和内部类成员同名,则内部类访问时采取就近原则,如果一定要访问外部类的同名成员,则采用 外部类.this.成员名 的方式 public class AnonymousClass { public static void main(String[] args) { // 外部其他类访问成员内部类的两种方式 // 方式一 通过外部类对象访问成员内部类对象,下面的两种写法本质上是等价的 Outer.Inner inner = new Outer().new Inner(); // 写法1 Outer outer = new Outer(); // 写法2 Outer.Inner inner1 = outer.new Inner(); // 方式二 在外部类中编写一个写法,返回成员内部类对象 Outer.Inner inner2 = outer.getInner(); } } class Outer{ private int n1 = 10; private String name = "张三"; public Inner getInner(){ return new Inner(); } // 成员内部类 // 成员内部类是定义在外部类的成员位置上 public class Inner{ private int n2 = 20; public void say(){ System.out.println("n1 = " + n1 + " name = " + name); } } public void show(){ Inner inner = new Inner(); // 即使是类内部的私有成员也可以被访问,因为本质上它也是类的一部分。 System.out.println(inner.n2); } } 静态内部类 静态内部类定义在外部类的成员位置,并且有 static 修饰符。 静态内部类可以直接访问外部类的所有静态成员(包括私有的),但是不能直接访问非静态成员;可以访问本类的所有成员,不管是静态还是非静态 解释: 静态内部类是外部类的一部分,因而可以访问外部类的所有静态成员。 对于外部类的非静态成员,可以在静态内部类中实例化,然后访问。 可以添加任意访问修饰符,因为它的地位就是一个成员 作用域和其他的成员一样,是整个类体内部。 静态内部类可以在不依赖外部类的前提下被实例化 外部其他类访问静态内部类、静态内部类访问外部类、外部类访问静态内部类的逻辑都写在了代码里,如下所示 外部类和静态内部类成员重名时,静态内部类如果想要访问,则默认就近原则,如果想要访问外部类的同名成员,则需要 外部类名.成员 P.S. 内部类可以是静态的,但是顶层类不能是静态的。 public class AnonymousClass { public static void main(String[] args) { // 展示外部其它类访问静态内部类的方式 // 方式1,直接new Outer.Inner inner = new Outer.Inner(); inner.sayHello(); // 方式2,编写一个方法,返回静态内部类的实例。 Outer.Inner inner1 = new Outer().getInnerInstance("java"); inner1.sayHello(); // 方式2的补充,可以在外部类中写静态方法,返回静态内部类的实例,没有本质区别 Outer.Inner inner2 = Outer.getInnerInstance_("HSP"); inner2.sayHello(); } } class Outer { private int n1 = 10; private static int n2 = 20; // 静态内部类 public static class Inner { private String name; private static int count = 0; private static int n2 = 30; // 静态内部类可以有构造器 Inner(String name) { this.name = name; } Inner() { } // 静态类里可以有普通方法 public void sayHello() { // 展示静态内部类访问外部类的方式。 // System.out.println(n1); 不能直接访问外部类非静态成员变量 System.out.println(new Outer().n1); // 但是可以通过创建外部类实例来访问外部类非静态变量 System.out.println(Outer.n2); // 可以直接访问外部类静态变量 System.out.println(n2); // 直接访问遵循就近原则 System.out.println("Hello, I'm " + this.name); // 也可以访问本类非静态变量 System.out.println("Total count: " + count); // 也可以访问本类静态变量 } } // 接下来展示外部类如何访问静态内部类 public void accessInner() { Inner inner = new Inner("HSP"); // 创建静态内部类对象 System.out.println(inner.name); // 访问静态内部类成员变量 System.out.println(Inner.count); // 访问静态内部类静态成员变量 } public Inner getInnerInstance(String name) { return new Inner(name); // 创建静态内部类对象并返回给调用方 } public static Inner getInnerInstance_(String name) { return new Inner(name); // 创建静态内部类对象并返回给调用方 } } 综合练习 练习1 当前代码会不会报错?为什么? public class Test { class Inner { public int a = 5; } public static void main(String[] args) { Inner r = new Inner(); } } 答案: 会报错,因为成员内部类在创建时依赖外部类的实例而存在,需要一个外部类作为容器,传统初始化方法为 “外部类实例.new 成员内部类名” main方法是静态的,静态方法中没有this关键字,因而报错,下面这样做就可以 public class Outer { public class Inner { } // 实例方法(非静态方法) public void show() { Inner inner = new Inner(); // ✅ 可以! // 等价于:this.new Inner(); // 因为实例方法中,this 指向当前 Outer 对象 } } 练习2 下面这段代码会输出什么? public class Test { public Test() { Inner s1 = new Inner(); s1.a = 10; Inner s2 = new Inner(); System.out.println(s2.a); } class Inner { public int a = 5; } public static void main(String[] args) { Test t = new Test(); Inner r = t.new Inner(); System.out.println(r.a); } } 答案: 5 5 分析: Test构造器里初始化的 S1 和 S2 分别是两个不同的成员内部类实例,而 main 方法里初始化的 r 也是一个不同的成员内部类实例。三个方法中的 a 是独立的 1 个帖子 - 1 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-09 12:59:15+08:00 · tech

朋友送了我一个广库的写字机, 商家给的配套软件是Windows版的’奎享雕刻’. 我家里大部分机器都是MacOS, 用起来还得专门开Windows的电脑, 非常不方便. 既然它的主程序是Java的, 我就直接上LLM进行破解提取了. 最初尝试用GPT-5.4, 用了ctf skill也不行, 道德感太强, 还是得请出D老师完成任务 最后花掉40多块, 终于完成了目标: 不需要登录, G-Code是本地生成的, 不依赖远程服务器. 有需要的佬友可以试试 奎享雕刻 v3.9.9 Java版 MacOS下需要自己安装JDK, Windows下软件内置了JRE 下载链接: drive.google.com kdraw.zip Google Drive file. 1 个帖子 - 1 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-08 15:45:42+08:00 · tech

背景:ERP系统中给一条合同创建回款单,回款单创建完成后更新合同的已收/未收金额字段。 业务:在审批场景下,可以给一条合同创建多条审批中的回款单(审批通过后才生效),为保证系统运行速度,用户点击审批同意后,主线程会去判断合同的未收金额是否足够,足够才能继续向下执行。但是审批会涉及到复杂的节点流转业务,所以更新合同已收/未收金额操作要通过MQ异步执行,从而提高响应速度。 问题:如果用户非常快的给同一合同的两条回款单点击同意,后同意的回款单很容易在主线程拿到先同意的回款单异步逻辑执行完成前的合同已收/未收金额,从而导致合同已收金额开超。 问了AI给出的方案不太符合现有需求,欢迎各位大佬理性讨论,各抒己见。(ps:前提一定要保证使用MQ更新合同金额) 4 个帖子 - 4 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-08 15:20:51+08:00 · tech

本人是即将毕业的应届生,之前一直做 Java 后端开发,实习中接触了影刀 RPA 后,被低代码自动化的高效玩法吸引了,现在想往 RPA 自动化方向深耕。 目前我已经掌握了影刀的基础流程搭建、数据处理、Excel 自动化、网页 / 桌面应用操作,也能用 Python 辅助做一些扩展开发。但还是对行业发展、进阶路径有点迷茫: 想了解 RPA 工程师在企业里的真实工作日常和晋升路径? 从 Java 转 RPA,我可以怎么发挥自己的后端优势,打造差异化竞争力? 应届生做 RPA,求职和学习上有什么需要注意的坑? 希望有前辈能分享下经验,也欢迎同做影刀的小伙伴一起交流学习! 1 个帖子 - 1 位参与者 阅读完整话题