-
本博客将带大家一起学习基本数据结构之一——栈(Stack),虽然Java当中的Stack集合已经被Deque(双端队列)替代了,但是他的基本思想和实现还是有必要学习的。一.初识栈1.基本概念 堆栈又名栈(stack),它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。 向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素; 从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。简单来讲,栈就是一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。 如下是它在Java集合框架中的位置: ps:由于Vector设计过时,所以继承自他的Stack也被替代了。2.特性LIFO:即Last In First Out,后进先出原则。类似于坐电梯,先走进去的人后出来;或者上子弹,最先进弹夹的子弹最后打出。3.核心操作入栈(push)、出栈(pop)、查看栈顶(peek)二.栈的模拟实现老规矩,先看源码: 我们不难发现栈的实现相当简单,底层就是一个数组,同时Stack类也相当简单,仅仅只有140余行。接下来我们不考虑泛型与io,存储的数据默认为int,来实现一个简单的栈,以理解栈的底层原理。1.经典实现最经典的就是基于数组的实现:(1)基本结构public class MyStack { private int[] elements; // 存储元素的数组 private int top; // 栈顶指针(初始为-1) private static final int DEFAULT_CAPACITY = 10; // 构造方法 public MyStack() { this(DEFAULT_CAPACITY); } public MyStack(int initialCapacity) { if (initialCapacity <= 0) { throw new IllegalArgumentException("容量必须为正数"); } this.elements = new int[initialCapacity]; top = -1; } ......AI生成项目java运行说明:由于是基于数组实现的,所以不得不考虑动态扩容机制。我们提供2种构造方法,一种指定初始容量,另一种不指定,使用默认容量,即DEFAULT_CAPACITY这一静态变量。我们提供一个指针来指示栈顶,即top。(2)动态扩容// 动态扩容private void resize(int newCapacity) { int[] newArray = new int[newCapacity]; System.arraycopy(elements, 0, newArray, 0, top + 1); elements = newArray;}AI生成项目java运行说明:System.arraycopy(elements, 0, newArray, 0, top + 1);AI生成项目java运行复制数组参数(原数组,复制起始位置,复制目的地,目的地起始位置,复制长度)(3)入栈(push)// 入栈(带动态扩容)public void push(int value) { // 检查是否需要扩容 if (top == elements.length - 1) { resize(2 * elements.length); } elements[++top] = value;}AI生成项目java运行(4)出栈(pop)// 出栈public int pop() { if (isEmpty()) { throw new IllegalStateException("栈为空"); } return elements[top--];}AI生成项目java运行(5)查看栈顶(peek)// 查看栈顶元素public int peek() { if (isEmpty()) { throw new IllegalStateException("栈为空"); } return elements[top];}AI生成项目java运行(6)其他// 判断是否为空public boolean isEmpty() { return top == -1;} // 获取元素数量public int size() { return top + 1;}AI生成项目java运行(7)完整实现与测试public class MyStack { private int[] elements; // 存储元素的数组 private int top; // 栈顶指针(初始为-1) private static final int DEFAULT_CAPACITY = 10; // 构造方法 public MyStack() { this(DEFAULT_CAPACITY); } public MyStack(int initialCapacity) { if (initialCapacity <= 0) { throw new IllegalArgumentException("容量必须为正数"); } elements = new int[initialCapacity]; top = -1; } // 入栈(带动态扩容) public void push(int value) { // 检查是否需要扩容 if (top == elements.length - 1) { resize(2 * elements.length); } elements[++top] = value; } // 出栈 public int pop() { if (isEmpty()) { throw new IllegalStateException("栈为空"); } return elements[top--]; } // 查看栈顶元素 public int peek() { if (isEmpty()) { throw new IllegalStateException("栈为空"); } return elements[top]; } // 判断是否为空 public boolean isEmpty() { return top == -1; } // 获取元素数量 public int size() { return top + 1; } // 动态扩容 private void resize(int newCapacity) { int[] newArray = new int[newCapacity]; System.arraycopy(elements, 0, newArray, 0, top + 1); elements = newArray; } // 测试代码 public static void main(String[] args) { MyStack stack = new MyStack(3); // 测试入栈和扩容 stack.push(10); stack.push(20); stack.push(30); stack.push(40); // 触发扩容到6 System.out.println("栈顶元素: " + stack.peek()); // 输出40 System.out.println("元素数量: " + stack.size()); // 输出4 // 测试出栈 System.out.println("出栈: " + stack.pop()); // 40 System.out.println("出栈: " + stack.pop()); // 30 System.out.println("剩余元素数量: " + stack.size()); // 2 }}AI生成项目java运行2.链表实现除了使用数组存储数据,使用链表也是可以的,并且使用链表不用考虑动态扩容。(1)基本结构public class MyLinkedStack { private static class Node { int data; Node next; Node(int data) { this.data = data; } } private Node top; // 栈顶节点 private int size; // 元素数量 ......AI生成项目java运行(2)入栈(push)public void push(int value) { Node newNode = new Node(value); newNode.next = top; // 新节点指向原栈顶 top = newNode; // 更新栈顶 size++;}AI生成项目java运行(3)出栈(pop)public int pop() { if (isEmpty()) { throw new IllegalStateException("栈为空"); } int value = top.data; top = top.next; // 移动栈顶指针 size--; return value;}AI生成项目java运行特别注意栈为空时会报错,所以要检查栈是否为空。(4)查看栈顶(peek)public int peek() { if (isEmpty()) { throw new IllegalStateException("栈为空"); } return top.data;}AI生成项目java运行(5)其他public boolean isEmpty() { return top == null;} public int size() { return size;}AI生成项目java运行(6)完整实现与测试public class MyLinkedStack { private static class Node { int data; Node next; Node(int data) { this.data = data; } } private Node top; // 栈顶节点 private int size; // 元素数量 public void push(int value) { Node newNode = new Node(value); newNode.next = top; // 新节点指向原栈顶 top = newNode; // 更新栈顶 size++; } public int pop() { if (isEmpty()) { throw new IllegalStateException("栈为空"); } int value = top.data; top = top.next; // 移动栈顶指针 size--; return value; } public int peek() { if (isEmpty()) { throw new IllegalStateException("栈为空"); } return top.data; } public boolean isEmpty() { return top == null; } public int size() { return size; } // 测试代码 public static void main(String[] args) { MyLinkedStack stack = new MyLinkedStack(); stack.push(100); stack.push(200); System.out.println(stack.pop()); // 200 System.out.println(stack.peek()); // 100 }}AI生成项目java运行三.栈的使用请见以下代码:import java.util.Stack; public class StackDemo { public static void main(String[] args) { // 1. 创建栈对象 Stack<Integer> stack = new Stack<>(); // 2. 压栈操作(push) System.out.println("----- 压栈操作 -----"); stack.push(10); stack.push(20); stack.push(30); System.out.println("当前栈内容: " + stack); // 输出: [10, 20, 30] // 3. 查看栈顶(peek) System.out.println("\n----- 查看栈顶 -----"); System.out.println("栈顶元素: " + stack.peek()); // 输出: 30 System.out.println("查看后栈内容: " + stack); // 保持原样 // 4. 弹栈操作(pop) System.out.println("\n----- 弹栈操作 -----"); System.out.println("弹出元素: " + stack.pop()); // 输出: 30 System.out.println("弹出后栈内容: " + stack); // 输出: [10, 20] // 5. 检查空栈(empty) System.out.println("\n----- 检查空栈 -----"); System.out.println("栈是否为空? " + stack.empty()); // 输出: false // 6. 搜索元素(search) System.out.println("\n----- 搜索元素 -----"); int target = 20; int position = stack.search(target); System.out.println("元素 " + target + " 的位置: " + position); // 输出: 1(栈顶为1) // 7. 清空栈 System.out.println("\n----- 清空栈 -----"); while (!stack.empty()) { System.out.println("弹出: " + stack.pop()); } System.out.println("清空后栈是否为空? " + stack.empty()); // 输出: true }}AI生成项目java运行更多信息请见官方文档说明:Stack (Java Platform SE 8 )四.栈的典型应用1.括号匹配算法该算法能自动检验输入的字符串中括号是否正确匹配:import java.util.Stack; public class BracketMatcher { public static boolean isValid(String s) { Stack<Character> stack = new Stack<>(); for (char c : s.toCharArray()) { // 遇到左括号时,将对应的右括号压入栈 switch (c) { case '(': stack.push(')'); break; case '[': stack.push(']'); break; case '{': stack.push('}'); break; default: // 遇到右括号时,检查栈顶是否匹配 if (stack.isEmpty() || stack.pop() != c) { return false; } } } // 最终栈必须为空才表示完全匹配 return stack.isEmpty(); }}AI生成项目java运行原理请见LeetCode:20. 有效的括号 - 力扣(LeetCode)2.逆波兰表达式(计算机的算数运算)import java.util.Stack; public class ReversePolishNotation { public static int evalRPN(String[] tokens) { Stack<Integer> stack = new Stack<>(); for (String token : tokens) { // 遇到运算符时进行计算 if (isOperator(token)) { int b = stack.pop(); int a = stack.pop(); stack.push(calculate(a, b, token)); } // 遇到数字时压栈 else { stack.push(Integer.parseInt(token)); } } return stack.pop(); } // 判断是否是运算符 private static boolean isOperator(String s) { return s.equals("+") || s.equals("-") || s.equals("*") || s.equals("/"); } // 执行运算(注意操作数顺序) private static int calculate(int a, int b, String op) { switch (op) { case "+": return a + b; case "-": return a - b; case "*": return a * b; case "/": return a / b; // 题目通常要求整数除法向零取整 default: throw new IllegalArgumentException("非法运算符"); } } public static void main(String[] args) { // 测试案例 String[][] testCases = { {"2","1","+","3","*"}, // (2+1)*3=9 {"4","13","5","/","+"}, // 4+(13/5)=6 {"10","6","9","3","+","-11","*","/","*","17","+","5","+"} // 10*(6/((9+3)*-11))+17+5 }; for (String[] testCase : testCases) { System.out.println("表达式: " + String.join(" ", testCase)); System.out.println("结果: " + evalRPN(testCase) + "\n"); } }}AI生成项目java运行详情请见:150. 逆波兰表达式求值 - 力扣(LeetCode)结语关于用Deque替代Stack的事————————————————原文链接:https://blog.csdn.net/2401_88030885/article/details/146369622
-
树树形结构:树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:有一个特殊的结点,称为根结点,根结点没有前驱结点。除根结点外,其余结点被分成M(M > 0)个互不相交的集合T1、T2、......、Tm,其中每一个集合Ti (1 <= i <= m)又是一棵与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继。树是递归定义的。注意:树形结构中,子树之间不能有交集,否则就不是树形结构。树的概念:结点的度:一个结点含有子树的个数称为该结点的度; 如上图:A的度为6树的度:一棵树中,所有结点度的最大值称为树的度; 如上图:树的度为6叶子结点或终端结点:度为0的结点称为叶结点; 如上图:B、C、H、I...等节点为叶结点双亲结点或父结点:若一个结点含有子结点,则这个结点称为其子结点的父结点; 如上图:A是B的父结点孩子结点或子结点:一个结点含有的子树的根结点称为该结点的子结点; 如上图:B是A的孩子结点根结点:一棵树中,没有双亲结点的结点;如上图:A结点的层次:从根开始定义起,根为第1层,根的子结点为第2层,以此类推树的高度或深度:树中结点的最大层次; 如上图:树的高度为4树的以下概念只需了解,在看书时只要知道是什么意思即可:非终端结点或分支结点:度不为0的结点; 如上图:D、E、F、G...等节点为分支结点兄弟结点:具有相同父结点的结点互称为兄弟结点; 如上图:B、C是兄弟结点堂兄弟结点:双亲在同一层的结点互为堂兄弟;如上图:H、I互为兄弟结点结点的祖先:从根到该结点所经分支上的所有结点;如上图:A是所有结点的祖先子孙:以某结点为根的子树中任一结点都称为该结点的子孙。如上图:所有结点都是A的子孙森林:由m(m>=0)棵互不相交的树组成的集合称为森林 二叉树概念:一棵二叉树是结点的一个有限集合,该集合:1. 或者为空2. 或者是由一个根节点加上两棵别称为左子树和右子树的二叉树组成。二叉树不存在度大于2的结点。 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树。对于任意的二叉树都是由以下几种情况复合而成的: 两种特殊的二叉树:1. 满二叉树: 一棵二叉树,如果每层的结点数都达到最大值,则这棵二叉树就是满二叉树。也就是说,如果一棵二叉树的层数为K,且结点总数是 2的k次方-1 ,则它就是满二叉树。2. 完全二叉树: 完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n 个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从0至n-1的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。 二叉树的性质:1. 若规定根结点的层数为1,则一棵非空二叉树的第i层上最多有 (i>0)个结点2. 若规定只有根结点的二叉树的深度为1,则深度为K的二叉树的最大结点数是 (k>=0)3. 对任何一棵二叉树, 如果其叶结点个数为 n0, 度为2的非叶结点个数为 n2,则有n0=n2+14. 具有n个结点的完全二叉树的深度k为 上取整5. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的顺序对所有节点从0开始编号,则对于序号为i 的结点有:若i>0,双亲序号:(i-1)/2;i=0,i为根结点编号,无双亲结点若2i+1,左孩子序号:2i+1,否则无左孩子若2i+2,右孩子序号:2i+2,否则无右孩子创建一个简单的二叉树:public class Main { public static void main(String[] args) { TreeNode<Character>a=new TreeNode<>('A'); TreeNode<Character>b=new TreeNode<>('B'); TreeNode<Character>c=new TreeNode<>('C'); TreeNode<Character>d=new TreeNode<>('D'); TreeNode<Character>e=new TreeNode<>('E'); a.left=b; a.right=c; b.left=d; b.right=e; System.out.println(a.left.left.element); } public static class TreeNode<E>{ public E element; public TreeNode<E> left,right; public TreeNode(E element){ this.element=element; } }} //输出DAI生成项目java运行二叉树的遍历所谓遍历(Traversal)是指沿着某条搜索路线,依次对树中每个结 点均做一次且仅做一次访问。访问结点所做的操作依赖于具体的应用问题(比如:打印节点内容、节点内容加 1)。 遍历是二叉树上最重要的操作之一,是二叉树上进行其它运算之基础。 前序遍历:打印根节点前序遍历左子树前序遍历右子树public class Main { public static void main(String[] args) { TreeNode<Character>a=new TreeNode<>('A'); TreeNode<Character>b=new TreeNode<>('B'); TreeNode<Character>c=new TreeNode<>('C'); TreeNode<Character>d=new TreeNode<>('D'); TreeNode<Character>e=new TreeNode<>('E'); TreeNode<Character>f=new TreeNode<>('F'); TreeNode<Character>g=new TreeNode<>('G'); TreeNode<Character>h=new TreeNode<>('H'); a.left=b; a.right=c; b.left=d; b.right=e; e.left=h; c.left=f; c.right=g; preOrder(a); } public static void preOrder(TreeNode<Character> root){ if(root==null)return; System.out.print(root.element+" "); preOrder(root.left); preOrder(root.right); } public static class TreeNode<E>{ public E element; public TreeNode<E> left,right; public TreeNode(E element){ this.element=element; } }}//输出A B D E H C F GAI生成项目java运行 ABDEHCFG中序遍历:中序遍历左子树打印结点中序遍历右子树public static void inOrder(TreeNode<Character>root){ if(root==null)return; inOrder(root.left); System.out.print(root.element+" "); inOrder(root.right); }//输出D B H E A F C G AI生成项目java运行DBEHAFCG后序遍历:后序遍历左子树后序遍历右子树打印结点 public static void postOrder(TreeNode<Character>root){ if(root==null)return; postOrder(root.left); postOrder(root.right); System.out.print(root.element+" "); }//输出D H E B F G C A AI生成项目java运行DHEBFGCA层序遍历:利用队列来实现层序遍历,首先将根节点存入队列中,接着循环执行以下步骤:进行出队操作,得到一个结点,并打印结点的值将此结点的左右孩子结点依次入队 public static void levelOrder(TreeNode<Character>root){ LinkedQueue<TreeNode<Character>> queue=new LinkedQueue<>(); //创建一个队列 queue.offer(root); //将根结点丢进队列 while (!queue.isEmpty()){ //如果队列不为空,就一直不断的取出来 TreeNode<Character>node=queue.poll(); //取一个出来 System.out.print(node.element+" "); //打印 if (node.left!=null)queue.offer(node.left); //如果左右孩子不为空,直接将左右孩子丢进队列 if (node.right!=null)queue.offer(node.right); } }//输出A B C D E F G H AI生成项目java运行 二叉查找树和平衡二叉树二叉查找树:二叉查找树也叫二叉搜索树或二叉排序树左子树中所有结点的值,均小于其根结点的值右子树中所有结点的值,均大于其根结点的值二叉搜索树的子树也是二叉搜索树平衡二叉树:在插入结点时要尽可能避免一边倒的情况,引入平衡二叉树的概念,在插入时如果不维护二叉树的平衡,某一边只会无限制的延伸下去,出现极度不平衡的情况。平衡二叉树一定是一颗二叉查找树任意结点的左右子树也是一颗平衡二叉树从根结点开始,左右子树高度差都不能超过1,否则视为不平衡二叉树上结点的左子树高度 减去 右子树高度,得到的结果称为该节点的平衡因子失衡情况的调整:1、LL型调整(右旋)2、RR型调整(左旋)3、RL型调整(先右旋再左旋)4、LR型调整(先左旋再右旋)红黑树红黑树也是二叉查找树的一种,结点有红有黑。规则1:每个结点可以是黑色或红色规则2:根结点一定是黑色规则3:红色结点的父结点和子结点不能为红色(不能有两个连续的红色)规则4:所有的空结点都是黑色(空结点视为null,红黑树中是将空结点视为叶子结点)规则5:每个结点到空结点路径上出现的黑色结点的个数都相等哈希表散列表散列(Hashing)通过散列函数(哈希函数)将需要参与检索的数据与散列值(哈希值)关联起来,生成一种便于搜索的数据结构,我们称其为散列表(哈希表)。散列函数也加哈希函数,哈希函数可以对一个目标计算出其对应的哈希值,并且,只要是同一个目标,无论计算多少次,得到的哈希值都是一样的结果,不同的目标计算出的结果几乎都不同,哈希函数在现实生活中应用十分广泛,比如很多下载网站都提供下载文件的MD5码校验,可以用来判别文件是否完整,哈希函数多种多样,目前应用最为广泛的是SHA-1和MD5。我们可以利用哈希值的特性,设计一张全新的表结构,这种表结构是专门为哈希设立的,我们称其为哈希表。我们可以将这些元素保存到哈希表中,而保存的位置则与其对应的哈希值有关,哈希值是通过哈希函数计算得到的,我们只需要将对应元素的关键字(一般是整数)提供给哈希函数就可以进行计算了,一般比较简单的哈希函数就是取模操作,哈希表长度是多少(长度最好是一个素数),模就是多少。保存的数据是无序的,哈希表在查找时只需要进行一次哈希函数计算就能直接找到对应元素的存储位置,效率极高。public class HashTable<E> { private final int TABLE_SIZE=10; private final Object[]TABLE=new Object[TABLE_SIZE]; //插入 public void insert(E obj){ int index=hash(obj); TABLE[index]=obj; } //判断是否包含 public boolean contains(E obj){ int index=hash(obj); return TABLE[index]==obj; } private int hash(E obj){ //哈希函数,计算出存放的位置 int hashCode=obj.hashCode(); //每一个对象都有一个独一无二的哈希值,可以通过hashCode方法得到(极小概率出现相同情况) return hashCode%TABLE_SIZE; }} import com.test.collection.HashTable; public static void main(String[] args) { HashTable<String>table=new HashTable<>(); String str="AAA"; System.out.println(table.contains(str)); table.insert(str); System.out.println(table.contains(str)); } //输出false//trueAI生成项目java运行通过哈希函数计算得到一个目标的哈希值,但是在某些情况下哈希值可能会出现相同的情况,称为哈希碰撞(哈希冲突)常见的哈希冲突解决方案是链地址法,当出现哈希冲突时,我们依然将其保存在对应的位置上,我们可以将其连接为一个链表的形式:package com.test.collection; public class HashTable<E> { private final int TABLE_SIZE=10; private final Node[]TABLE=new Node[TABLE_SIZE]; //放入头结点 public HashTable(){ for (int i = 0; i < TABLE_SIZE; i++) TABLE[i]=new Node<>(null); } //插入 public void insert(E obj){ int index=hash(obj); Node<E>head=TABLE[index]; Node<E>node=new Node<>(obj); node.next=head.next; head.next=node; } //判断是否包含 public boolean contains(E element){ int index=hash(element); Node<E>node=TABLE[index].next; while (node!=null){ if(node.element==element) return true; node=node.next; } return false; } private int hash(E obj){ //哈希函数,计算出存放的位置 int hashCode=obj.hashCode(); //每一个对象都有一个独一无二的哈希值,可以通过hashCode方法得到(极小概率出现相同情况) return hashCode%TABLE_SIZE; } public String toString(){ StringBuilder builder=new StringBuilder(); for (int i = 0; i < TABLE_SIZE; i++) { Node<E>head=TABLE[i].next; while (head!=null){ builder.append(head.element+"->"); head=head.next; } builder.append("\n"); } return builder.toString(); } private static class Node<E>{ private final E element; private Node<E> next; private Node(E element){ this.element=element; } }}AI生成项目java运行 public static void main(String[] args) { HashTable<Integer>table1=new HashTable<>(); for (int i = 0; i < 100; i++) table1.insert(i); System.out.println(table1); } /*输出90->80->70->60->50->40->30->20->10->0->91->81->71->61->51->41->31->21->11->1->92->82->72->62->52->42->32->22->12->2->93->83->73->63->53->43->33->23->13->3->94->84->74->64->54->44->34->24->14->4->95->85->75->65->55->45->35->25->15->5->96->86->76->66->56->46->36->26->16->6->97->87->77->67->57->47->37->27->17->7->98->88->78->68->58->48->38->28->18->8->99->89->79->69->59->49->39->29->19->9->————————————————原文链接:https://blog.csdn.net/PluMage11/article/details/147387863
-
向现实世界要答案现实世界里,账户转账操作是支持并发的,而且绝对是真正的并行,银行所有的窗口都可以做转账操作。只要我们能仿照现实世界做转账操作,串行的问题就解决了。我们试想在古代,没有信息化,账户的存在形式真的就是一个账本,而且每个账户都有一个账本,这些账本都统一存放在文件架上。银行柜员在给我们做转账时,要去文件架上把转出账本和转入账本都拿到手,然后做转账。这个柜员在拿账本的时候可能遇到以下三种情况:文件架上恰好有转出账本和转入账本,那就同时拿走;如果文件架上只有转出账本和转入账本之一,那这个柜员就先把文件架上有的账本拿到手,同时等着其他柜员把另外一个账本送回来;转出账本和转入账本都没有,那这个柜员就等着两个账本都被送回来。上面这个过程在编程的世界里怎么实现呢?其实用两把锁就实现了,转出账本一把,转入账本另一把。在transfer()方法内部,我们首先尝试锁定转出账户this(先把转出账本拿到手),然后尝试锁定转入账户target(再把转入账本拿到手),只有当两者都成功时,才执行转账操作。这个逻辑可以图形化为下图这个样子。 两个转账操作并行示意图而至于详细的代码实现,如下所示。经过这样的优化后,账户A 转账户B和账户C 转账户D这两个转账操作就可以并行了。class Account { private int balance; // 转账 void transfer(Account target, int amt){ // 锁定转出账户 synchronized(this) { // 锁定转入账户 synchronized(target) { if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } } }} 运行本项目没有免费的午餐上面的实现看上去很完美,并且也算是将锁用得出神入化了。相对于用Account.class作为互斥锁,锁定的范围太大,而我们锁定两个账户范围就小多了,这样的锁,上一章我们介绍过,叫 细粒度锁。 使用细粒度锁可以提高并行度,是性能优化的一个重要手段。这个时候可能你已经开始警觉了,使用细粒度锁这么简单,有这样的好事,是不是也要付出点什么代价啊?编写并发程序就需要这样时时刻刻保持谨慎。的确,使用细粒度锁是有代价的,这个代价就是可能会导致死锁。在详细介绍死锁之前,我们先看看现实世界里的一种特殊场景。如果有客户找柜员张三做个转账业务:账户A 转账户B 100元,此时另一个客户找柜员李四也做个转账业务:账户B 转账户A 100 元,于是张三和李四同时都去文件架上拿账本,这时候有可能凑巧张三拿到了账本A,李四拿到了账本B。张三拿到账本A后就等着账本B(账本B已经被李四拿走),而李四拿到账本B后就等着账本A(账本A已经被张三拿走),他们要等多久呢?他们会永远等待下去…因为张三不会把账本A送回去,李四也不会把账本B送回去。我们姑且称为死等吧。转账业务中的“死等”现实世界里的死等,就是编程领域的死锁了。 死锁 的一个比较专业的定义是: 一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象。上面转账的代码是怎么发生死锁的呢?我们假设线程T1执行账户A转账户B的操作,账户A.transfer(账户B);同时线程T2执行账户B转账户A的操作,账户B.transfer(账户A)。当T1和T2同时执行完①处的代码时,T1获得了账户A的锁(对于T1,this是账户A),而T2获得了账户B的锁(对于T2,this是账户B)。之后T1和T2在执行②处的代码时,T1试图获取账户B的锁时,发现账户B已经被锁定(被T2锁定),所以T1开始等待;T2则试图获取账户A的锁时,发现账户A已经被锁定(被T1锁定),所以T2也开始等待。于是T1和T2会无期限地等待下去,也就是我们所说的死锁了。class Account { private int balance; // 转账 void transfer(Account target, int amt){ // 锁定转出账户 synchronized(this){ ① // 锁定转入账户 synchronized(target){ ② if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } } }} 运行本项目关于这种现象,我们还可以借助资源分配图来可视化锁的占用情况(资源分配图是个有向图,它可以描述资源和线程的状态)。其中,资源用方形节点表示,线程用圆形节点表示;资源中的点指向线程的边表示线程已经获得该资源,线程指向资源的边则表示线程请求资源,但尚未得到。转账发生死锁时的资源分配图就如下图所示,一个“各据山头死等”的尴尬局面。 转账发生死锁时的资源分配图如何预防死锁并发程序一旦死锁,一般没有特别好的方法,很多时候我们只能重启应用。因此,解决死锁问题最好的办法还是规避死锁。那如何避免死锁呢?要避免死锁就需要分析死锁发生的条件,有个叫Coffman的牛人早就总结过了,只有以下这四个条件都发生时才会出现死锁:互斥,共享资源X和Y只能被一个线程占用;占有且等待,线程T1已经取得共享资源X,在等待共享资源Y的时候,不释放共享资源X;不可抢占,其他线程不能强行抢占线程T1占有的资源;循环等待,线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源,就是循环等待。反过来分析, 也就是说只要我们破坏其中一个,就可以成功避免死锁的发生。其中,互斥这个条件我们没有办法破坏,因为我们用锁为的就是互斥。不过其他三个条件都是有办法破坏掉的,到底如何做呢?对于“占用且等待”这个条件,我们可以一次性申请所有的资源,这样就不存在等待了。对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。对于“循环等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了。我们已经从理论上解决了如何预防死锁,那具体如何体现在代码上呢?下面我们就来尝试用代码实践一下这些理论。1. 破坏占用且等待条件从理论上讲,要破坏这个条件,可以一次性申请所有资源。在现实世界里,就拿前面我们提到的转账操作来讲,它需要的资源有两个,一个是转出账户,另一个是转入账户,当这两个账户同时被申请时,我们该怎么解决这个问题呢?可以增加一个账本管理员,然后只允许账本管理员从文件架上拿账本,也就是说柜员不能直接在文件架上拿账本,必须通过账本管理员才能拿到想要的账本。例如,张三同时申请账本A和B,账本管理员如果发现文件架上只有账本A,这个时候账本管理员是不会把账本A拿下来给张三的,只有账本A和B都在的时候才会给张三。这样就保证了“一次性申请所有资源”。 通过账本管理员拿账本对应到编程领域,“同时申请”这个操作是一个临界区,我们也需要一个角色(Java里面的类)来管理这个临界区,我们就把这个角色定为Allocator。它有两个重要功能,分别是:同时申请资源apply()和同时释放资源free()。账户Account 类里面持有一个Allocator的单例(必须是单例,只能由一个人来分配资源)。当账户Account在执行转账操作的时候,首先向Allocator同时申请转出账户和转入账户这两个资源,成功后再锁定这两个资源;当转账操作执行完,释放锁之后,我们需通知Allocator同时释放转出账户和转入账户这两个资源。具体的代码实现如下。class Allocator { private List<Object> als = new ArrayList<>(); // 一次性申请所有资源 synchronized boolean apply( Object from, Object to){ if(als.contains(from) || als.contains(to)){ return false; } else { als.add(from); als.add(to); } return true; } // 归还资源 synchronized void free( Object from, Object to){ als.remove(from); als.remove(to); }} class Account { // actr应该为单例 private Allocator actr; private int balance; // 转账 void transfer(Account target, int amt){ // 一次性申请转出账户和转入账户,直到成功 while(!actr.apply(this, target)) ; try{ // 锁定转出账户 synchronized(this){ // 锁定转入账户 synchronized(target){ if (this.balance > amt){ this.balance -= amt; target.balance += amt; } } } } finally { actr.free(this, target) } }} 运行本项目2. 破坏不可抢占条件破坏不可抢占条件看上去很简单,核心是要能够主动释放它占有的资源,这一点synchronized是做不到的。原因是synchronized申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。你可能会质疑,“Java作为排行榜第一的语言,这都解决不了?”你的怀疑很有道理,Java在语言层次确实没有解决这个问题,不过在SDK层面还是解决了的,java.util.concurrent这个包下面提供的Lock是可以轻松解决这个问题的。关于这个话题,咱们后面会详细讲。3. 破坏循环等待条件破坏这个条件,需要对资源进行排序,然后按序申请资源。这个实现非常简单,我们假设每个账户都有不同的属性 id,这个 id 可以作为排序字段,申请的时候,我们可以按照从小到大的顺序来申请。比如下面代码中,①~⑥处的代码对转出账户(this)和转入账户(target)排序,然后按照序号从小到大的顺序锁定账户。这样就不存在“循环”等待了。class Account { private int id; private int balance; // 转账 void transfer(Account target, int amt){ Account left = this Account right = target; if (this.id > target.id) { left = target; right = this; } // 锁定序号小的账户 synchronized(left){ // 锁定序号大的账户 synchronized(right){ if (this.balance > amt){ this.balance -= amt; target.balance += amt; } } } }} 运行本项目总结当我们在编程世界里遇到问题时,应不局限于当下,可以换个思路,向现实世界要答案, 利用现实世界的模型来构思解决方案,这样往往能够让我们的方案更容易理解,也更能够看清楚问题的本质。但是现实世界的模型有些细节往往会被我们忽视。因为在现实世界里,人太智能了,以致有些细节实在是显得太不重要了。在转账的模型中,我们为什么会忽视死锁问题呢?原因主要是在现实世界,我们会交流,并且会很智能地交流。而编程世界里,两个线程是不会智能地交流的。所以在利用现实模型建模的时候,我们还要仔细对比现实世界和编程世界里的各角色之间的差异。我们今天这一篇文章主要讲了 用细粒度锁来锁定多个资源时,要注意死锁的问题。这个就需要你能把它强化为一个思维定势,遇到这种场景,马上想到可能存在死锁问题。当你知道风险之后,才有机会谈如何预防和避免,因此, 识别出风险很重要。预防死锁主要是破坏三个条件中的一个,有了这个思路后,实现就简单了。但仍需注意的是,有时候预防死锁成本也是很高的。例如上面转账那个例子,我们破坏占用且等待条件的成本就比破坏循环等待条件的成本高,破坏占用且等待条件,我们也是锁了所有的账户,而且还是用了死循环 while(!actr.apply(this, target)); 方法,不过好在apply()这个方法基本不耗时。 在转账这个例子中,破坏循环等待条件就是成本最低的一个方案。所以我们在选择具体方案的时候,还需要 评估一下操作成本,从中选择一个成本最低的方案。————————————————原文链接:https://blog.csdn.net/2402_87298751/article/details/150211720
-
前言在使用GitHub时,看到好的项目或想给某个项目做贡献,此时通常会将代码仓库fork到自己的账号下。如果在此期间,如果源仓库的代码发生了变动,就需要与源仓库代码进行同步。这里实操一下,如何实现这一操作。 配置项目的上游仓库首先需要大家将fork的仓库代码clone到本地,后面的所有操作都是基于本地代码库来进行操作的。比如,可以通过git clone先将fork的代码下载到本地:git clone git@github.com:secbr/nacos.git后续的一步步操作,都是基于本地仓库来进行操作。 进入到本地仓库目录通过cd操作,进入到clone下来的本地仓库根目录:cd /Users/apple/develop/nacos-request/nacos后续的操作无特殊说明,都是在这个本地仓库的目录下进行操作。 查看远程仓库路径执行命令 git remote -v 查看远程仓库的路径:appledeMacBook-Pro-2:nacos apple$ git remote -vorigin https://github.com/secbr/nacos.git (fetch)origin https://github.com/secbr/nacos.git (push)如果只显示2行内容,说明该项目还未设置upstream(中文叫:上游代码库),一般情况下,设置好一次upstream后就无需重复设置。通过显示远程仓库的路径和clone时的路径对照,会发现,此时远程仓库的路径还是fork项目的路径。 添加upstream路径执行命令 git remote add upstream https://xxx.git,把fork的源仓库设置为 upstream 。这里项目是从alibaba的nacos仓库fork过来的,因此对应的upstream就是alibaba的源仓库地址。执行上述命令,在此执行git remote -v 检查是否成功。appledeMacBook-Pro-2:nacos apple$ git remote add upstream https://github.com/alibaba/nacos.gitappledeMacBook-Pro-2:nacos apple$ git remote -vorigin https://github.com/secbr/nacos.git (fetch)origin https://github.com/secbr/nacos.git (push)upstream https://github.com/alibaba/nacos.git (fetch)upstream https://github.com/alibaba/nacos.git (push)通过上面的输出可以看出,多了两项upstream的地址,说明添加upstream成功。 检查本地代码状态由于实例是直接从仓库clone下来的,本地还没有修改代码。如果本地项目已经修改了一些代码,不确定是否提交了代码,就需要执行git status来检查一下。appledeMacBook-Pro-2:nacos apple$ git statusOn branch developYour branch is up to date with 'origin/develop'. nothing to commit, working tree clean上面显示,本地没有需要提交的(commit)的代码。如果本地有修改,需要先从本地仓库推送到GitHub仓库。然后,再执行一次 git status 检查。对应推送到GitHub仓库的基本操作步骤如下:git add -A 或者 git add filenamegit commit -m "your note"git push origin mastergit status完成上面的基本操作之后,确认代码都已经提交,便可以开始执行源仓库与本地仓库的merge操作了。 抓取源仓库的更新经过上面步骤的准备之后,可以进行源仓库的代码更新了。执行命令 git fetch upstream 抓取原仓库的更新:appledeMacBook-Pro-2:nacos apple$ git fetch upstreamremote: Enumerating objects: 2646, done.remote: Counting objects: 100% (2593/2593), done.remote: Compressing objects: 100% (1157/1157), done.remote: Total 2646 (delta 731), reused 2404 (delta 682), pack-reused 53Receiving objects: 100% (2646/2646), 1.67 MiB | 1.47 MiB/s, done.Resolving deltas: 100% (734/734), completed with 37 local objects.From https://github.com/alibaba/nacos* [new branch] 0.2.1 -> upstream/0.2.1* [new branch] 0.2.2 -> upstream/0.2.2* [new branch] 0.3.0 -> upstream/0.3.0//...省略一部分执行上述命令之后,上游仓库的更新(commit)会本存储为本地的分支,通常名称为:upstream/BRANCHNAME。比如上面的upstream/0.3.0。 切换分支完成了上游仓库分支的拉取之后,先来核查一下本地仓库当前处于哪个分支,也就是需要更新合并的分支。比如,这里需要将develop分支的内容更新到与上游仓库代码一致。则先切换到develop分支:appledeMacBook-Pro-2:nacos apple$ git checkout developAlready on 'develop'Your branch is up to date with 'origin/develop'.上面提示已经是develop分支了。 执行合并执行命令 git merge upstream/develop 合并远程的develop分支。比如合并的可能是master,可根据需要将develop的名称替换成对应的master。appledeMacBook-Pro-2:nacos apple$ git merge upstream/developRemoving test/src/test/java/com/alibaba/nacos/test/naming/DeregisterInstance_ITCase.java// ...省略一部分Removing naming/src/test/java/com/alibaba/nacos/naming/core/PushServiceTest.javaAuto-merging client/src/main/java/com/alibaba/nacos/client/naming/remote/http/NamingHttpClientProxy.javaCONFLICT (content): Merge conflict in client/src/main/java/com/alibaba/nacos/client/naming/remote/http/NamingHttpClientProxy.javaRemoving client/src/main/java/com/alibaba/nacos/client/naming/core/HostReactor.javaRemoving .editorconfigAutomatic merge failed; fix conflicts and then commit the result.执行完上述命令之后,会发现上游代码指定分支的修改内容已经反映到本地代码了。 上传代码到fork分支执行完上一步的合并操作之后,往后还有一些后续处理,比如代码冲突。如果本地修改了内容,上游仓库也修改了对应的代码,则可能会出现冲突。这时就需要对比代码进行修改。本人更习惯使用IDEA中可视化的插件进行代码冲突解决,也可以选择喜欢的方式进行解决。解决完冲突之后,就可以执行正常的代码add、commit和push操作了。这里的一系列操作都是针对自己fork的仓库的,对应操作实例如下:appledeMacBook-Pro-2:nacos apple$ git add .appledeMacBook-Pro-2:nacos apple$ git commit -m 'merge from nacos'[develop 8601c1791] merge from nacos appledeMacBook-Pro-2:nacos apple$ git pushEnumerating objects: 4, done.Counting objects: 100% (4/4), done.Delta compression using up to 12 threadsCompressing objects: 100% (2/2), done.Writing objects: 100% (2/2), 281 bytes | 281.00 KiB/s, done.Total 2 (delta 1), reused 0 (delta 0), pack-reused 0remote: Resolving deltas: 100% (1/1), completed with 1 local object.To https://github.com/secbr/nacos.git76a4dcbb1..8601c1791 develop -> develop上述操作,通过add、commit、push一系列操作,将源仓库中的修改内容,提交到自己fork的分支当中了。此时再查看自己fork的GitHub仓库,可以发现代码已经更新。
-
在Java开发的复杂生态中,尤其是当使用Spring框架来构建强大的应用程序时,异常情况就像隐藏在暗处的荆棘,随时可能阻碍开发进程。其中,org.springframework.beans.factory.BeanCreationException这个异常犹如一位不速之客,常常在Bean的创建阶段引发混乱。它的出现意味着在Spring容器尝试创建Bean的过程中遇到了严重问题,而这些问题可能隐藏在看似简单的配置或者复杂的代码逻辑之中。这不仅让开发者们感到困惑,也可能影响整个应用程序的正常启动和运行。那么,如何才能像经验丰富的侦探一样,迅速找出问题所在并解决这个令人头疼的报错呢?本文将为您详细剖析这个异常,提供一系列实用的解决方案。一、问题描述1.1 报错示例以下是一个简单但具有代表性的场景,可能导致org.springframework.beans.factory.BeanCreationException异常的出现:假设我们有一个简单的Java类,代表一个用户信息的实体类:1234567891011121314151617package com.example.model;public class User { private String username; private int age; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public int getAge() { return age; } public void setAge(int age) { this.age = age; }}然后,我们创建一个Spring的配置文件(beans.xml)来配置这个User类的Bean:12345678910<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="user" class="com.example.model.User"> <property name="username" value="John Doe"/> <property name="age" value="abc"/> <!-- 这里故意设置一个错误的值类型,应为整数 --> </bean></beans>最后,我们创建一个简单的测试类来获取这个Bean:1234567891011package com.example.test;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;import com.example.model.User;public class Main { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); User user = (User) context.getBean("user"); System.out.println("Username: " + user.getUsername() + ", Age: " + user.getAge()); }}在这个示例中,由于在配置文件中为age属性设置了一个错误类型的值(字符串abc而不是整数),当Spring尝试创建user Bean时,就可能会抛出org.springframework.beans.factory.BeanCreationException异常。1.2 报错分析在上述示例中,导致org.springframework.beans.factory.BeanCreationException异常的原因主要是由于Bean属性配置错误。1.2.1 类型不匹配问题在配置user Bean时,对于age属性,Spring期望一个整数类型的值,但配置文件中提供了一个字符串。当Spring尝试将字符串abc转换为整数来设置age属性时,转换失败,从而导致Bean创建过程中的异常。这种类型不匹配问题在Spring的属性注入机制中是一个常见的错误来源。1.2.2 Bean依赖问题(假设存在依赖)如果User类依赖于其他的Bean,而这些依赖Bean在创建过程中出现问题(例如,依赖Bean的配置错误、无法找到依赖Bean的类等),也会导致user Bean创建失败。比如,如果User类有一个Address类型的属性,而Address Bean的配置存在问题,那么在创建user Bean时,当Spring尝试注入Address Bean时就会出现异常。1.2.3 类加载问题有时候,即使Bean本身的配置看起来正确,但如果类加载出现问题,也会导致Bean无法创建。例如,如果User类所在的jar包损坏或者类路径配置错误,Spring在尝试加载User类时会失败,进而引发BeanCreationException。这可能是由于项目构建问题、IDE设置问题或者运行环境问题导致的。1.3 解决思路解决这个问题的思路主要是从Bean的配置、依赖关系以及类加载等方面进行全面排查。1.3.1 检查Bean属性配置仔细检查配置文件中对Bean属性的设置,包括属性值的类型、格式等是否正确。确保每个属性的值都能正确地转换为Bean类中相应属性的类型。1.3.2 审查Bean依赖关系对于有依赖的Bean,检查依赖Bean的配置是否正确,是否存在依赖缺失或者配置错误的情况。可以通过查看Spring的启动日志或者使用调试工具来追踪依赖Bean的创建过程。1.3.3 排查类加载问题确认Bean相关类的类路径是否正确,检查项目的构建路径、IDE设置以及运行环境的配置。确保类能够被正确加载,没有受到损坏的jar包或者错误的类路径影响。二、解决方法2.1 方法一:修正Bean属性配置错误2.1.1 类型检查与修正在上述示例中,对于user Bean的age属性,将配置文件中的错误值修改为正确的整数类型。修改后的配置如下:12345678910<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="user" class="com.example.model.User"> <property name="username" value="John Doe"/> <property name="age" value="30"/> </bean></beans>这样,Spring在创建user Bean时就能正确地设置age属性,避免因类型不匹配导致的异常。2.1.2 语法和格式检查除了类型问题,还要检查属性配置的语法和格式。例如,如果属性值是一个复杂的表达式或者引用其他Bean,确保语法正确。如果使用SpEL(Spring Expression Language)表达式,检查表达式的语法和引用是否正确。2.2 方法二:解决Bean依赖问题2.2.1 检查依赖Bean的配置如果User类依赖于其他Bean,如Address Bean,检查Address Bean的配置文件。确保Address Bean的类名、属性配置等都正确。例如,如果Address Bean的配置如下:1234<bean id="address" class="com.example.model.Address"> <property name="street" value="123 Main St"/> <property name="city" value="Anytown"/></bean>检查Address类是否存在,属性street和city是否与Address类中的属性匹配,以及类路径是否正确。2.2.2 处理依赖缺失或错误如果发现依赖Bean缺失(例如,忘记配置某个依赖Bean),添加正确的配置。如果依赖Bean的配置存在错误,如类名错误或者属性设置问题,修改相应的配置。另外,如果存在循环依赖问题(即两个或多个Bean相互依赖),可以采用合适的设计模式或者使用Spring的@Lazy注解来解决。例如,如果A Bean依赖于B Bean,B Bean又依赖于A Bean,可以在其中一个Bean的依赖注入处使用@Lazy注解,延迟该Bean的初始化,以避免循环依赖导致的创建失败。2.3 方法三:解决类加载问题2.3.1 检查项目构建路径在使用IDE(如Eclipse、IntelliJ IDEA等)开发时,检查项目的构建路径设置。确保所有包含Bean相关类的源文件目录、依赖的jar包等都正确添加到构建路径中。在Eclipse中,可以通过项目属性中的“Java Build Path”选项来检查和修改构建路径。在IntelliJ IDEA中,可以在项目结构设置中查看和调整相关内容。2.3.2 检查运行环境和类路径当应用程序在服务器或者其他运行环境中部署时,检查运行环境的类路径设置。确保所有必要的jar包都在类路径中,并且没有冲突或损坏的jar包。如果使用了Maven或Gradle等构建工具,检查生成的可执行jar包或war包的结构,确保类和依赖的jar包都正确打包。如果发现类加载问题是由于损坏的jar包导致的,可以尝试重新下载或更新相关的jar包。2.3.3 查看类加载日志大多数Java应用服务器和运行环境都提供了类加载的日志功能。可以查看这些日志来确定是否存在类加载失败的情况。例如,在Tomcat服务器中,可以查看catalina.out或localhost.log等日志文件,查找有关类加载问题的信息。根据日志中的错误信息,如“ClassNotFoundException”或“java.lang.NoClassDefFoundError”,进一步排查问题所在。2.4 方法四:使用Spring的调试和日志功能2.4.1 启用详细的Spring日志在项目的日志配置文件(如log4j.properties或logback.xml等)中,将Spring相关的日志级别设置为DEBUG或TRACE。例如,在log4j.properties中,可以添加以下内容:1log4j.logger.org.springframework=DEBUG这样,在应用程序启动和运行过程中,Spring会输出更详细的日志信息,包括Bean的创建过程、属性注入情况、依赖关系处理等。通过查看这些详细日志,可以更清晰地发现问题所在。例如,如果Bean创建失败,日志中可能会显示出具体是哪个属性配置错误、哪个依赖Bean无法创建或者是类加载过程中的哪个步骤出现问题。2.4.2 使用Spring的调试工具(在IDE中)许多现代的IDE(如IntelliJ IDEA)提供了对Spring的调试支持。在调试模式下启动应用程序,可以在IDE中设置断点,观察Spring在创建Bean过程中的内部操作。可以查看变量的值、方法的调用顺序等,帮助确定问题的具体位置。例如,可以在Spring创建user Bean的过程中,在属性注入的代码处设置断点,查看注入的值是否正确,以及是否有异常抛出。三、其他解决方法检查与其他框架或组件的集成:如果应用程序使用了多个框架或组件,并且它们与Spring有交互,检查这些交互是否导致了BeanCreationException。例如,如果使用Spring与Hibernate集成,检查Hibernate的配置是否与Spring的配置相互影响,是否存在版本冲突或者配置不兼容的问题。确保所有相关框架和组件的配置都正确协调,不会干扰Spring的Bean创建过程。清理和重新构建项目:有时候,由于编译过程中的临时文件、缓存或者其他原因,可能会导致Bean创建出现问题。可以尝试在IDE中清理项目(通常有专门的清理选项),然后重新构建项目。这可以清除可能存在的错误编译产物,重新生成干净的类文件和配置文件。在使用Maven或Gradle等构建工具时,也可以使用相应的清理和重新构建命令(如mvn clean install或gradle clean build)。检查Bean的生命周期方法(如果存在):如果Bean实现了InitializingBean接口或者定义了init - method等生命周期方法,检查这些方法中的代码是否存在问题。例如,如果在InitializingBean的afterPropertiesSet方法中抛出异常,也会导致Bean创建失败。同样,对于destroy - method(如果有)也要进行检查,确保在Bean的整个生命周期中不会出现影响其创建的问题。可以在这些生命周期方法中添加日志输出,或者使用调试工具来检查方法的执行情况。四、总结本文围绕org.springframework.beans.factory.BeanCreationException这个在Spring应用程序开发中常见的异常展开了深入的讨论。通过详细的代码示例展示了可能导致该异常的场景,包括Bean属性配置错误、Bean依赖问题以及类加载问题等。分析了这些问题引发异常的原因,强调了在Bean创建过程中各个环节出现问题的可能性。提出了多种解决方法,如修正Bean属性配置错误、解决Bean依赖问题、解决类加载问题以及使用Spring的调试和日志功能等。此外,还介绍了检查与其他框架或组件的集成、清理和重新构建项目以及检查Bean的生命周期方法等其他相关的解决途径。下次遇到这类报错时,首先应该查看Spring的日志信息(如果已启用详细日志),以确定问题的大致方向。然后从Bean的属性配置、依赖关系、类加载以及与其他框架的集成等方面入手,逐一排查可能出现的问题。结合本文提到的各种方法,全面检查和修复问题,确保Spring能够顺利创建Bean,保障应用程序的正常启动和运行。在开发和维护过程中,保持对配置文件和代码的仔细审查,以及对运行环境和框架集成的关注,有助于预防和及时解决这类异常情况,提高应用程序的稳定性和可靠性。
-
一、方法1:使用重定向1、在命令行中,你可以使用重定向操作符 > 或 >> 来将输出重定向到文件中。例如:12$ java -jar example.jar > output.log$ java -jar example.jar >> output.log2、这会将标准输出(stdout)重定向到 output.log 文件。如果你想同时捕获标准错误(stderr),可以使用 2>&1 来合并标准错误到标准输出:12java -jar example.jar > output.log 2>&1Java -jar example.jar >> output.log 2>&1二、方法2:在代码中配置日志框架1、如果你使用的是如 Log4j、SLF4J、Logback 等日志框架,你可以在代码中配置日志的输出目的地。例如,使用 Logback 的 logback.xml 配置文件:1234567891011<configuration> <appender name="FILE" class="ch.qos.logback.core.FileAppender"> <file>path/to/your/logfile.log</file> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="debug"> <appender-ref ref="FILE" /> </root></configuration>2、确保将 标签中的路径改为你的目标文件路径。三、方法3:使用 JVM 参数指定日志文件1、某些日志框架允许通过 JVM 参数来指定日志文件。例如,使用 Log4j 2,你可以在启动时通过系统属性来设置日志文件:1java -D log4j.configurationFile=path/to/log4j2.xml -jar example.jar2、其中 log4j2.xml 应该包含一个类似于上面 Logback 配置的配置,指定输出到特定文件。四、方法4:使用第三方库或工具对于一些复杂的场景,你可能会想要使用更高级的日志管理工具,如 Logrotate(在 Linux 上)或者使用第三方 Java 库如 log4j-layout-tpl 来实现更复杂的日志轮转和归档策略。例如,使用 Logrotate 可以自动管理日志文件的大小和轮转。1、示例:使用 Log4j2 的 XML 配置文件确保你的 example.jar 包含了 Log4j2 的依赖,并创建一个 log4j2.xml 文件在你的项目资源目录中(例如 src/main/resources),内容如下:12345678910111213<?xml version="1.0" encoding="UTF-8"?><Configuration> <Appenders> <File name="LogFile" fileName="path/to/your/logfile.log"> <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/> </File> </Appenders> <Loggers> <Root level="info"> <AppenderRef ref="LogFile"/> </Root> </Loggers></Configuration>2、然后,在运行你的 jar 时指定 Log4j2 的配置文件:1java -Dlog4j.configurationFile=path/to/log4j2.xml -jar example.jar3、这样,你的应用日志就会输出到指定的文件了。
-
Redis 提供了 Pub/Sub (发布/订阅) 模式,允许客户端订阅频道并接收发布到这些频道的消息。以下是 Java 中使用 Redis 实现消息订阅的几种方式。1. 使用 Jedis 客户端添加依赖12345<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>4.3.1</version></dependency>基本订阅示例12345678910111213141516171819202122232425import redis.clients.jedis.Jedis;import redis.clients.jedis.JedisPubSub; public class RedisSubscriber { public static void main(String[] args) { // 创建 Jedis 连接 Jedis jedis = new Jedis("localhost", 6379); // 创建订阅者 JedisPubSub subscriber = new JedisPubSub() { @Override public void onMessage(String channel, String message) { System.out.println("收到消息 - 频道: " + channel + ", 内容: " + message); } @Override public void onSubscribe(String channel, int subscribedChannels) { System.out.println("订阅成功 - 频道: " + channel); } }; // 订阅频道 jedis.subscribe(subscriber, "myChannel"); }}发布消息123456789import redis.clients.jedis.Jedis; public class RedisPublisher { public static void main(String[] args) { Jedis jedis = new Jedis("localhost", 6379); jedis.publish("myChannel", "Hello, Redis Pub/Sub!"); jedis.close(); }}2. 使用 Lettuce 客户端 (推荐)Lettuce 是另一个流行的 Redis Java 客户端,支持响应式编程。添加依赖12345<dependency> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> <version>6.2.3.RELEASE</version></dependency>订阅示例12345678910111213141516171819202122232425262728293031323334353637383940414243import io.lettuce.core.RedisClient;import io.lettuce.core.pubsub.RedisPubSubListener;import io.lettuce.core.pubsub.StatefulRedisPubSubConnection;import io.lettuce.core.pubsub.api.sync.RedisPubSubCommands; public class LettuceSubscriber { public static void main(String[] args) { RedisClient client = RedisClient.create("redis://localhost"); StatefulRedisPubSubConnection<String, String> connection = client.connectPubSub(); connection.addListener(new RedisPubSubListener<String, String>() { @Override public void message(String channel, String message) { System.out.println("收到消息 - 频道: " + channel + ", 内容: " + message); } @Override public void message(String pattern, String channel, String message) { // 模式匹配的消息 } @Override public void subscribed(String channel, long count) { System.out.println("订阅成功 - 频道: " + channel); } // 其他需要实现的方法... }); RedisPubSubCommands<String, String> sync = connection.sync(); sync.subscribe("myChannel"); // 保持程序运行以持续接收消息 try { Thread.sleep(Long.MAX_VALUE); } catch (InterruptedException e) { e.printStackTrace(); } connection.close(); client.shutdown(); }}3. Spring Data Redis 集成如果你使用 Spring Boot,可以更方便地集成 Redis Pub/Sub,这也是比较常用的方式添加依赖1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency>配置 Redis 容器123456789101112131415161718import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.listener.ChannelTopic;import org.springframework.data.redis.listener.RedisMessageListenerContainer; @Configurationpublic class RedisConfig { @Bean public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory, RedisMessageSubscriber subscriber) { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(connectionFactory); container.addMessageListener(subscriber, new ChannelTopic("myChannel")); return container; }}配置订阅123456789101112import org.springframework.data.redis.connection.Message;import org.springframework.data.redis.connection.MessageListener;import org.springframework.stereotype.Component; @Componentpublic class RedisMessageSubscriber implements MessageListener { @Override public void onMessage(Message message, byte[] pattern) { System.out.println("收到消息: " + new String(message.getBody())); }}发布消息12345678910111213141516import org.springframework.data.redis.core.RedisTemplate;import org.springframework.stereotype.Service; @Servicepublic class RedisMessagePublisher { private final RedisTemplate<String, Object> redisTemplate; public RedisMessagePublisher(RedisTemplate<String, Object> redisTemplate) { this.redisTemplate = redisTemplate; } public void publish(String message) { redisTemplate.convertAndSend("myChannel", message); }}高级功能模式匹配订阅:可以使用 psubscribe 订阅匹配模式的频道取消订阅处理连接断开:实现 onPMessage, onPUnsubscribe 等方法处理各种事件1234567891011//模式匹配订阅// Jedisjedis.psubscribe(subscriber, "news.*"); // Lettucesync.psubscribe("news.*"); //取消订阅subscriber.unsubscribe("myChannel");subscriber.punsubscribe("news.*");
-
log.info("requestBody: " + finalBody); 这段代码在 finalBody 为 null 时会抛出 NullPointerException,因为 Java 中不能对 null 调用 String 的方法(如 + 拼接操作实际上会调用 String.valueOf(),但直接拼接 null 字符串是允许的,但这里可能涉及日志框架的特殊处理)。具体分析如果 log.info() 内部直接拼接字符串:"requestBody: " + finalBody 在 finalBody == null 时会返回字符串 "requestBody: null"(不会报错,因为 Java 的 + 操作符允许拼接 null)。但某些日志框架(如 SLF4J、Log4j)可能对 null 有特殊处理,如果 log.info() 内部尝试调用 finalBody.toString(),则会抛出 NullPointerException。如果 log.info() 使用 {} 占位符(推荐方式):log.info("requestBody: {}", finalBody); // 更安全,日志框架会自动处理 null 这种方式不会报错,日志框架(如 SLF4J)会将 null 转换为 "null" 字符串输出。如何避免 NullPointerException?方案 1:使用 {} 占位符(推荐)log.info("requestBody: {}", finalBody); // 自动处理 null 方案 2:手动判空log.info("requestBody: " + (finalBody != null ? finalBody : "null")); 方案 3:使用 String.valueOf()(避免 NPE)log.info("requestBody: " + String.valueOf(finalBody)); // null 会变成 "null" 结论如果 log.info() 直接拼接字符串(+ 操作符),通常不会报错,但取决于日志框架的实现。最安全的方式是使用 {} 占位符(如 SLF4J),它能正确处理 null 值。如果日志框架不支持 {} 占位符,建议手动判空或使用 String.valueOf()。示例(SLF4J/Logback)import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Example { private static final Logger log = LoggerFactory.getLogger(Example.class); public static void main(String[] args) { String finalBody = null; log.info("requestBody: {}", finalBody); // 输出: requestBody: null log.info("requestBody: " + finalBody); // 通常不会报错,但取决于日志框架 } } 推荐使用 {} 占位符,避免潜在的 NullPointerException。
-
Java服务使用了读写分离地址,代码中使用事务注解,会强制走主库吗?
-
1.modelengine提供哪些模型?最新版本ds 和 千问32.上下文限制8096问题3.知识库是在mate里面,还是在engine里面,如果都有怎么使用4.意图识别准确率。是否必选项
-
问题1.园区数字平台24.1现在有两套环境x86和ARM,并且ARM环境才有AI相关内容,如果让我们对接两套环境,业务无法闭环(AI算法调用ARM但是业务数据在X86)?问题2.cv模型算法是否可以在24.1版本进行对接,是否对接万州算法(人员摔倒、人员聚集、车辆违停、消防通道占用、烟火监测等),请给出具体对接方式和demo?问题3.安全红线现在有很多底层框架扫描问题,比如扫描spring boot2.x有安全漏洞,是否能提供java侧底层框架推荐版本建议,比如spring boot2.x可以升级到哪个版本,spring boot3.x升级到哪个版本?
-
简介:JDK 1.8(Java 8)是Oracle公司推出的Java SE重要版本,包含了众多提升开发效率和代码可维护性的创新特性。中文版API文档为中文开发者提供详尽的接口说明,涵盖了lambda表达式、方法引用、默认方法、Stream API、新日期时间API、Optional类、Nashorn JavaScript引擎和新并发工具等核心特性。该文档是理解和运用Java 8新特性的关键资源,搭配使用说明文件,能够帮助开发者快速掌握新特性,并提升编程效率和代码质量。 1. JDK 1.8版本概述JDK 1.8,也被称为Java Platform, Standard Edition 8,是Oracle公司于2014年发布的重要Java版本更新。它引入了多项创新特性,如lambda表达式、Stream API、新的日期时间API以及改进的并发工具等。这些新特性旨在简化Java代码,提高开发效率,以及更好地适应现代编程的多核处理器环境。1.8版本的核心目标之一是提升Java的性能,并增强函数式编程的支持。JDK 1.8不仅增强了集合框架的处理能力,还为Java语言带来了强大的函数式编程能力,这主要通过lambda表达式来实现。另外,JDK 1.8也对Java的类型系统、注解处理以及接口设计等方面进行了重要的更新和改进。JDK 1.8的这些新特性对现有的Java开发工作流程和架构设计带来了显著的变化。开发者可以利用这些新工具和API来编写更为简洁、高效和可读性更强的代码。例如,lambda表达式和Stream API使得集合操作更直观,而新的日期时间API则让时间处理变得更加灵活和强大。graph TD; JDK8[开始] JDK8 --> 新特性引入[引入新特性] 新特性引入 --> 性能优化[性能优化] 性能优化 --> 函数式编程[函数式编程] 函数式编程 --> 现代化开发[现代化开发体验] 现代化开发 --> 结束AI生成项目mermaid在接下来的章节中,我们将深入探讨JDK 1.8带来的主要特性,如lambda表达式、Stream API、新的日期时间API等,以及它们是如何改变Java编程方式的。本章节为读者提供了一个概览,为深入理解后续章节内容奠定了基础。2. lambda表达式使用2.1 lambda表达式基本概念2.1.1 lambda表达式的定义与格式Lambda表达式是Java 8中引入的一种简洁表达函数式编程的方式。它提供了一种更灵活、更简洁的接口实现方法。Lambda表达式的基本语法格式如下:(parameters) -> expressionAI生成项目java运行或者当有多条语句时:(parameters) -> { statements; }AI生成项目java运行其中,参数列表可以包含零个或多个参数,参数类型可以明确指出,也可以省略不写。箭头符号( -> )分隔参数列表和表达式主体。表达式主体可以是一个返回语句,也可以是一个方法调用。Lambda表达式简化了编写只有一个抽象方法的接口(函数式接口)的实例的代码。这使得代码更加简洁易读,并且减少了冗余的样板代码。2.1.2 lambda表达式与匿名类的对比Lambda表达式与匿名类在很多方面都具有相似的功能,但它们之间存在一些本质的区别。匿名类是Java早期版本中实现单方法接口的常用方式。与匿名类相比,Lambda表达式更加简洁,因为它无需显式声明类型,也不需要使用关键字 new 创建对象。例如,在使用匿名类时,我们可能会写出类似这样的代码:Comparator<String> comparator = new Comparator<String>() { @Override public int compare(String s1, String s2) { return Integer.compare(s1.length(), s2.length()); }};AI生成项目java运行而使用Lambda表达式后,代码可以简化为:Comparator<String> comparator = (s1, s2) -> Integer.compare(s1.length(), s2.length());AI生成项目java运行这种简化使得代码更加直观,并且提高了代码的可读性。2.2 lambda表达式深入理解2.2.1 函数式接口的理解与应用函数式接口是指只有一个抽象方法的接口,它可以被隐式地转换为Lambda表达式。Java中一些常用的函数式接口包括 Consumer 、 Function 、 Predicate 等。函数式接口通常配合Lambda表达式一起使用,以实现更加简洁的代码。例如:Function<String, Integer> function = String::length;AI生成项目java运行这里, Function 接口的 apply 方法被隐式地通过Lambda表达式 String::length 实现,这个方法接受一个字符串参数并返回该字符串的长度。2.2.2 lambda表达式的延迟加载与变量捕获Lambda表达式除了可以捕获其代码块中使用的外部局部变量外,还具有延迟执行的特性。Lambda表达式可以在定义后延迟到实际需要执行时才运行。例如:int[] numbers = {1, 2, 3, 4, 5};IntStream stream = Arrays.stream(numbers);Consumer<Integer> print = number -> System.out.println(number);stream.forEach(print); // 只有在这时才会执行print的操作AI生成项目java运行在这段代码中, print Lambda表达式定义了对局部变量 number 的操作,但只有在 stream.forEach(print); 时才会实际执行。2.3 lambda表达式在集合框架中的应用2.3.1 集合的函数式编程操作Java 8为集合框架添加了大量使用Lambda表达式的函数式编程方法。这些方法通常在 Collection 接口的子接口中实现,如 List 和 Set 。例如:List<String> names = Arrays.asList("Alice", "Bob", "Charlie");names.forEach(name -> System.out.println(name));AI生成项目java运行在这段代码中, forEach 方法接受一个Consumer类型的Lambda表达式来遍历集合中的每个元素。2.3.2 案例分析:集合操作的简化实现利用Lambda表达式和新的集合操作API,可以极大地简化集合操作的代码。例如,过滤集合中的元素、映射元素到新形式、计算集合元素的统计信息等:List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);List<Integer> evenNumbers = numbers.stream() .filter(n -> n % 2 == 0) .collect(Collectors.toList());AI生成项目java运行这段代码使用了Stream API和Lambda表达式来过滤出集合中所有的偶数。以上章节内容展示了Lambda表达式在Java 8中的强大功能和灵活应用,从基本概念到深入理解和集合框架中的运用,每一个环节都紧密相扣,旨在为读者提供全面的理解和操作实践。接下来的章节将深入探讨方法引用和构造器引用,进一步展开函数式编程的魅力。3. 方法引用与构造器引用3.1 方法引用基础3.1.1 方法引用的定义及类型方法引用是一种简写lambda表达式的方式,它允许你直接引用现有的方法,并且可以作为参数传递给方法。Java 8的lambda表达式支持函数式接口,而方法引用正是对函数式接口实例化的一种更简洁的表达方式。与lambda表达式相比,方法引用通过名称直接指向一个方法,减少了代码量,也使得代码更加直观。方法引用主要有以下几种类型:引用静态方法: ContainingClass::staticmethod引用某个对象的方法: containingObject::instanceMethod引用特定类型的方法: ContainingType::methodName引用构造函数: ClassName::new每种类型的方法引用都与相应的lambda表达式等价。3.1.2 方法引用与lambda表达式的关系方法引用实际上可以看做是lambda表达式的一种特殊形式。当你有一个lambda表达式,比如 (arg1, arg2, ... ) -> ClassName.staticMethod(arg1, arg2, ...) ,你可以通过方法引用来简化它,用 ClassName::staticMethod 来代替。如果这个lambda表达式只调用了单一的方法,没有额外的逻辑处理,那么方法引用提供了一种更加简洁的方式来实现相同的功能。3.2 方法引用高级应用3.2.1 方法引用的使用场景与优势方法引用的使用场景通常是在lambda表达式中只对传入的参数进行方法调用,而不添加任何额外操作时。使用方法引用的优势在于:代码简洁性 :更简洁的表达形式让代码易于阅读和维护。清晰表达意图 :方法引用使得代码的意图更加明显,开发者可以通过方法名快速了解其功能。减少错误 :由于方法引用是对已存在的方法的引用,减少了在lambda表达式中编写方法体的可能错误。3.2.2 结合流API的方法引用实例在Java 8中, Stream API提供了大量的方法可以和方法引用结合使用。例如,假设有一个 Employee 类,有一个 getSalary() 方法,我们可以使用方法引用来替代lambda表达式计算所有员工工资的总和:List<Employee> employees = // ...初始化员工列表double totalSalary = employees.stream() .map(Employee::getSalary) .reduce(0.0, Double::sum);AI生成项目java运行上面的代码通过 Employee::getSalary 方法引用替代了原本使用lambda表达式 e -> e.getSalary() 的方式,使得代码更加简洁。3.3 构造器引用详解3.3.1 构造器引用的定义与实现构造器引用是对类构造器的引用,这允许你直接引用一个特定的构造器,而不必显式地在代码中调用它。构造器引用的语法是 ClassName::new ,它与使用lambda表达式创建类实例的效果一样。例如,假设有一个 Person 类,它有一个构造器 Person(String name, int age) ,我们可以使用构造器引用来创建 Person 对象:Supplier<Person> personSupplier = Person::new;Person person = personSupplier.get("John Doe", 25);AI生成项目java运行在这个例子中, Person::new 是一个构造器引用,表示我们要使用 Person 类的构造器来创建对象。通过调用 get 方法,我们实际上调用了 Person 的构造器,创建了一个 Person 实例。3.3.2 构造器引用与工厂模式的结合构造器引用非常适合和工厂模式结合使用。工厂模式允许创建对象而不直接使用new关键字,可以隐藏构造器的实现细节,提供更好的封装。结合构造器引用,可以使得工厂方法更加简洁:public interface PersonFactory { Person createPerson(String name, int age);} // 实现工厂类public class PersonFactoryImpl implements PersonFactory { @Override public Person createPerson(String name, int age) { return new Person(name, age); }} // 使用构造器引用创建工厂实例PersonFactory factory = Person::new; // 使用工厂实例创建Person对象Person person = factory.createPerson("Jane Doe", 30);AI生成项目java运行在这个例子中,我们定义了一个 PersonFactory 接口,它有一个 createPerson 方法,我们通过构造器引用来实现这个接口,从而创建了一个 Person 对象。这种方式使得代码更加模块化和可维护。4. 接口中的默认方法4.1 默认方法的定义与特点4.1.1 默认方法的概念与重要性默认方法是Java 8中引入的一个重要特性,它允许在接口中直接定义方法的实现。这种设计使得在不破坏现有实现的情况下,可以为接口添加新的方法。默认方法通过在接口中使用 default 关键字来声明,并提供方法体实现。这样的改变极大程度上提升了接口的灵活性,使得接口可以在保持向后兼容的前提下不断演进。默认方法主要解决两个问题:一是允许向现有的接口添加新的方法而不破坏已有的实现;二是支持函数式编程,允许在接口中使用方法引用。4.1.2 默认方法与多重继承的问题解决在Java 8之前,Java不支持多重继承,即一个类不能有多个父类。这主要是为了避免在继承体系中出现方法签名冲突时的不确定性。然而,在多态和设计模式中,多重继承的需求是客观存在的。默认方法的引入,为实现类似多重继承的效果提供了可能。通过在接口中定义默认方法,一个类可以实现多个接口,并且当这些接口中存在具有相同方法签名的默认方法时,可以通过覆盖这些默认方法来解决潜在的冲突,从而在不改变类层次结构的情况下实现多重继承的效果。4.2 默认方法的实践应用4.2.1 接口默认方法的使用场景默认方法的使用场景通常出现在两个方面:一是为了向后兼容,扩展已有的接口;二是为了支持函数式编程,提供更多的工具方法。在实际开发中,可以利用默认方法实现以下功能:在集合框架中,提供集合操作的通用方法。实现接口的简化的模板方法。提供默认的函数式接口行为,使得接口可以更加灵活。默认方法的引入,让接口变得更为丰富,同时也让Java的编程模型更加接近函数式编程。4.2.2 集合框架中的默认方法实例分析在Java集合框架中, Collection 接口增加了几个默认方法,比如 removeIf 、 forEach 、 replaceAll 和 spliterator 等。这些方法提供了一种新的、更加简洁的方式来操作集合。以 forEach 方法为例,它提供了一个简单的遍历集合的方式,例如:collection.forEach(System.out::println);AI生成项目java运行这段代码使用了方法引用 System.out::println ,这是Java 8中引入的另一个新特性,与 forEach 方法结合使用,可以方便地打印集合中的每个元素。这比使用传统的迭代方式来遍历集合更为简洁和直观。4.3 默认方法与继承的关系4.3.1 默认方法与类继承的冲突处理当一个类实现的多个接口中存在具有相同方法签名的默认方法时,就会出现冲突。在这种情况下,Java编译器需要类提供一个明确的方法实现来解决这种冲突。对于冲突的解决,有以下几种策略:类可以提供自己的方法实现,覆盖掉接口中定义的默认方法。类可以使用 super 关键字调用某个特定接口的默认方法。如果冲突的方法在类的父类中也有实现,则父类的实现将被优先使用。冲突解决策略的灵活性使得默认方法的设计更加实用,允许开发者在保持接口更新的同时,也能够维护代码的稳定性和可维护性。4.3.2 继承中的默认方法覆盖策略覆盖默认方法是处理接口冲突的一种常见方式。当开发者不希望使用接口提供的默认实现时,可以选择在子类中明确覆盖这个默认方法。例如:public class MyCollection<E> extends AbstractCollection<E> { @Override public void forEach(Consumer<? super E> action) { // 自定义实现覆盖默认方法 }}AI生成项目java运行在这个例子中, MyCollection 类覆盖了 forEach 方法,从而实现了自定义的行为。这种覆盖可以是完全重写接口中的默认方法,也可以是调用其他接口的默认方法来补充或修改当前接口中的默认方法。通过这种方式,类可以灵活地处理接口的默认方法冲突,并提供最适合自己的实现方式。graph LR A[接口A] -->|默认方法| B[类B] A -->|默认方法| C[类C] B -->|覆盖默认方法| D[类B自己的方法] C -->|覆盖默认方法| E[类C自己的方法] D -->|调用父类方法| F[父类方法] E -->|调用其他接口默认方法| G[接口D的默认方法]AI生成项目mermaid在上述流程图中,展示了默认方法覆盖的策略。类B和类C都继承自接口A,并覆盖了接口A中的默认方法。类B覆盖后调用了父类的方法,而类C则选择了调用其他接口的默认方法。这样的设计提供了高度的灵活性,允许开发者根据实际需求做出最合适的选择。5. Stream API应用5.1 Stream API基础5.1.1 Stream API简介与特点Stream API是Java 8引入的一个强大的编程接口,旨在以声明式的方式处理数据集合。与传统的循环相比,Stream API能够更加简洁、高效地处理集合数据。它将操作分为中间操作(intermediate operations)和终端操作(terminal operations),其中中间操作会返回一个新的流(Stream),而终端操作则是最终的数据处理步骤,它会返回一个非流的结果,例如一个List或计算出的总和。Stream API支持函数式编程风格,允许开发者将对集合的操作以链式调用的方式串联起来,形成一个流水线,最终一次性进行处理。这种风格在提高代码的可读性的同时,也支持并行处理,有助于提高程序运行效率。5.1.2 Stream API的基本操作与流程Stream API的基本操作可以从创建流开始,然后可以进行一系列的中间操作来处理流中的数据,最后通过一个终端操作来产生结果。以下是一个典型的Stream API操作流程:List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");long count = names.stream() // 创建流 .filter(name -> name.startsWith("A")) // 中间操作:筛选操作 .map(String::toUpperCase) // 中间操作:转换操作 .count(); // 终端操作:计算流中元素个数AI生成项目java运行在上述代码中,我们首先将一个List转换为Stream,然后使用 .filter() 方法筛选出所有以"A"开头的名字,接着使用 .map() 方法将名字转换为大写,最后通过 .count() 方法计算出符合条件的名字数量。整个过程是延迟执行的,只有在进行终端操作时,中间操作才会按顺序执行。5.2 Stream API深入探究5.2.1 流的中间操作详解中间操作包括但不限于 .filter() , .map() , .flatMap() , .sorted() , .limit() , 和 .skip() 等,它们用于在流的元素上执行转换、过滤、排序等操作。每个中间操作都会返回一个新的流,你可以继续链式调用其他中间操作。例如,以下是一个中间操作的链式调用示例:List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);List<Integer> evenNumbers = numbers.stream() .filter(n -> n % 2 == 0) // 筛选偶数 .sorted() // 排序 .collect(Collectors.toList()); // 收集结果到ListAI生成项目java运行在这个例子中,我们首先对数字列表进行筛选,得到一个仅包含偶数的新流,然后对这些偶数进行排序,最终将它们收集到一个新的List中。5.2.2 流的终端操作及其应用终端操作是流操作流程的最后阶段,它们会触发流的最终计算并返回结果。常见的终端操作包括 .forEach() , .reduce() , .collect() , .min() , .max() , 和 .anyMatch() 等。终端操作不能继续产生新的流,且是流操作的触发点。一旦执行终端操作,与之相关的流就会关闭,无法再次被使用。例如,使用 .forEach() 来遍历流中的元素,并执行一些操作:numbers.stream().forEach(System.out::println);AI生成项目java运行在这个例子中, .forEach() 是终端操作,它会打印流中的每个元素。5.3 Stream API的性能考量5.3.1 Stream API与传统循环的性能对比Stream API相较于传统的循环结构,不仅可以提高代码的可读性,还能在一些情况下提升性能。例如,当流操作可以利用并行执行时,性能提升尤为明显。然而,并行流并不总是比顺序流更快。对于数据量较小或者操作较为简单的场景,传统的循环可能反而更优。在选择使用Stream API时,需要根据实际情况考虑是否使用并行流以及如何优化流操作,比如减少中间操作的数量、避免不必要地装箱操作等。5.3.2 Stream API的优化技巧与最佳实践Stream API的优化通常涉及以下几个方面:减少中间操作 :每个中间操作都会产生一个新的流,这可能导致效率低下。尝试减少中间操作的数量。并行流的正确使用 :并行流可以提升大数据集处理的性能,但是开销也很大。并行流的使用需要根据具体情况进行测试。避免装箱操作 :原始类型的流比对象类型的流处理速度快。尽量使用原始类型的流,比如 IntStream , LongStream , DoubleStream 等。终端操作的合理选择 :不同的终端操作有不同的性能影响,需要根据实际需求选择合适的终端操作。通过以上优化技巧,可以确保在使用Stream API时获得最佳的性能表现。6. 新日期时间API使用6.1 Java 8之前的日期时间问题回顾Java 8之前的日期时间处理API一直被开发者所诟病,主要是因为 java.util.Date 类以及 Calendar 类存在诸多设计上的缺陷。这些问题导致了在处理日期和时间时,代码往往显得冗长且不够直观。6.1.1 旧日期时间API的痛点与不足不可变性缺失 :旧的日期时间类不是不可变的,这使得日期时间对象在多线程环境下容易出问题。设计不合理 : Date 类既是日期类又是时间戳类,容易引起混淆。线程安全问题 :旧的日期时间类并不是线程安全的,这要求开发者自行处理同步问题。格式化与解析限制 :旧API在日期格式化和解析方面功能有限,需要大量自定义代码。时区处理复杂 :时区处理不够直观,容易出现时区错误。6.1.2 新旧API的对比与迁移指南Java 8引入了全新的日期时间API,以解决旧API存在的问题。新的API位于 java.time 包中,提供了以下改进:不可变且线程安全 :新的日期时间类是不可变的,并且许多类都是线程安全的。清晰的职责分配 :日期、时间和时区各自有不同的类,职责分明。强大的格式化和解析能力 :新的日期时间API提供了灵活的日期时间格式化和解析能力。改善的时区支持 :时区处理变得简单直接,例如 ZonedDateTime 类可以清晰地表示包含时区的日期时间。易于迁移 :虽然新的API是推荐的解决方案,但Java提供了工具类 java.time.format.DateTimeFormatterBuilder ,以帮助在新旧API之间迁移。新的日期时间API不仅解决了旧API的痛点,还提供了更符合现代编程习惯的日期时间处理能力。这使得代码更加简洁、可读,并且更易于维护。6.2 新日期时间API详解6.2.1 java.time 包中的关键类介绍Java 8的 java.time 包提供了处理日期和时间的核心类。关键类包括:LocalDate :用于表示没有时间没有时区的日期。LocalTime :用于表示没有日期的时间。LocalDateTime :结合了 LocalDate 和 LocalTime ,表示没有时区的日期和时间。ZonedDateTime :在 LocalDateTime 的基础上增加了时区信息。Instant :用于表示时间点上的瞬间(UTC时区)。Duration :用于表示两个时间点之间的持续时间。Period :用于表示两个日期之间的年、月、日的时间段。6.2.2 日期时间的解析与格式化操作新的日期时间API支持通过 DateTimeFormatter 类进行解析和格式化操作。下面是一个例子:import java.time.LocalDateTime;import java.time.format.DateTimeFormatter;import java.time.format.DateTimeFormatterBuilder;import java.util.Locale; public class DateTimeExample { public static void main(String[] args) { // 定义日期时间格式 DateTimeFormatter formatter = new DateTimeFormatterBuilder() .parseCaseInsensitive() .appendPattern("yyyy-MM-dd HH:mm:ss") .toFormatter(Locale.US); // 解析日期时间字符串 LocalDateTime dateTime = LocalDateTime.parse("2023-03-20 14:30:00", formatter); // 格式化日期时间对象为字符串 String formattedDateTime = dateTime.format(formatter); System.out.println("Formatted Date Time: " + formattedDateTime); }}AI生成项目java运行这个例子中,我们定义了一个日期时间格式,并使用它来解析和格式化 LocalDateTime 对象。 DateTimeFormatterBuilder 类提供了更多的灵活性和控制,允许构建复杂的日期时间格式器。6.3 新日期时间API高级特性6.3.1 时区处理与时间调整器的应用Java 8的 java.time 包也提供了强大的时区处理能力。时区通常由 ZoneId 类表示,它与 ZonedDateTime 类结合使用以处理时区特定的日期时间。import java.time.ZonedDateTime;import java.time.ZoneId; public class ZoneDateTimeExample { public static void main(String[] args) { // 获取当前时区的日期时间 ZonedDateTime nowInDefaultZone = ZonedDateTime.now(); System.out.println("Current date time in default zone: " + nowInDefaultZone); // 获取特定时区的日期时间 ZonedDateTime nowInNewYork = ZonedDateTime.now(ZoneId.of("America/New_York")); System.out.println("Current date time in New York: " + nowInNewYork); }}AI生成项目java运行在这个例子中,我们获取了默认时区和纽约时区的当前日期时间。时间调整器由 TemporalAdjuster 接口和 TemporalAdjusters 类提供,允许调整日期时间到下一个工作日、月末等。import java.time.LocalDate;import java.time.temporal.TemporalAdjusters; public class TemporalAdjusterExample { public static void main(String[] args) { // 调整到下个月的第一天 LocalDate date = LocalDate.now(); date = date.with(TemporalAdjusters.firstDayOfNextMonth()); System.out.println("First day of next month: " + date); }}AI生成项目java运行通过使用 TemporalAdjuster ,开发者可以进行复杂的日期时间调整操作,大大简化了日期时间的计算工作。6.3.2 日期时间API的扩展包介绍Java 8的日期时间API非常强大,但在某些特定情况下可能还需要额外的处理能力。为此,Joda-Time库提供了扩展API,虽然Java 8已经内置了强大的日期时间处理能力,但在需要更专业的领域,如金融行业中,Joda-Time仍然有其使用场景。Joda-Time库提供了更为丰富的时间调整器、更精确的日期时间操作等。但在Java 8已经内置了强大的日期时间处理能力的当下,它的重要性有所下降。尽管如此,了解Joda-Time还是对深入理解日期时间概念有帮助。import org.joda.time.DateTime;import org.joda.time.DateTimeZone;import org.joda.time.Interval; public class JodaTimeExample { public static void main(String[] args) { // 使用Joda-Time获取当前时间 DateTime now = new DateTime(DateTimeZone.UTC); System.out.println("Current time in Joda-Time: " + now); // 创建一个时间间隔 DateTime start = now.minusHours(1); DateTime end = now.plusHours(1); Interval interval = new Interval(start, end); System.out.println("Interval between one hour before and after now: " + interval); }}AI生成项目java运行在这个例子中,我们使用Joda-Time库获取了当前的UTC时间,并创建了一个间隔一个小时的时间间隔。通过本章的学习,我们了解了Java 8之前日期时间处理的不足,掌握了新日期时间API的基本使用方法,并探讨了其高级特性和扩展包。这为我们高效准确地处理日期时间问题提供了坚实的基础。本文还有配套的精品资源,点击获取 简介:JDK 1.8(Java 8)是Oracle公司推出的Java SE重要版本,包含了众多提升开发效率和代码可维护性的创新特性。中文版API文档为中文开发者提供详尽的接口说明,涵盖了lambda表达式、方法引用、默认方法、Stream API、新日期时间API、Optional类、Nashorn JavaScript引擎和新并发工具等核心特性。该文档是理解和运用Java 8新特性的关键资源,搭配使用说明文件,能够帮助开发者快速掌握新特性,并提升编程效率和代码质量————————————————原文链接:https://blog.csdn.net/weixin_42588555/article/details/147877112
-
一、引言1.1 定义与类型适配器模式是一种结构型设计模式,主要目的是将一个类的接口转换为客户期望的另一个接口。这种模式使得原本因为接口不匹配而不能一起工作的类可以一起工作,从而提高了类的复用性。适配器模式分为类适配器和对象适配器两种类型。类适配器使用继承关系来实现,而对象适配器则使用组合关系。适配器模式的核心在于解决接口不兼容的问题。在软件系统中,随着应用环境的变化,常常需要将一些现存的对象放在新的环境中应用,但是新环境要求的接口是这些现存对象所不满足的。适配器模式通过将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。这种模式在促进现有类的复用方面发挥了重要作用。例如,假设你有一个使用旧接口的类库,而这个接口与你的新系统不兼容。通过适配器模式,你可以创建一个适配器类,将旧接口转换为新系统所需的接口,从而在新系统中复用旧类库的功能。这不仅提高了代码的复用性,还减少了开发新功能所需的时间和精力。1.2 模式的作用适配器模式的主要作用是在不修改原有类的情况下,通过适配器类来匹配新的接口需求。这不仅保留了原有类的功能,也为新环境的集成提供了可能。此外,适配器模式还可以用来实现多态性,客户端可以通过目标接口调用不同的适配器,从而实现不同的功能。适配器模式在接口转换和类复用方面发挥着关键作用。它允许开发人员在不修改现有代码的情况下,使不同接口的类能够协同工作。这种模式通过提供一个中间层(适配器),将不兼容的接口转换为可兼容的接口,从而实现类的复用和系统的灵活扩展。例如,在企业级应用中,经常需要集成不同供应商提供的组件。这些组件可能具有不同的接口,无法直接集成到系统中。通过使用适配器模式,开发人员可以创建适配器类,将这些不同接口转换为系统统一的接口,从而实现组件的集成和复用。这不仅提高了系统的灵活性,还减少了开发和维护的成本。二、类适配器模式2.1 结构类适配器模式通过多重继承的方式实现。在这种模式中,适配器类同时继承目标接口和需要适配的类,从而实现接口的转换。类适配器的结构包括目标接口、需要适配的类和适配器类。适配器类既是目标接口的子类,又是适配类的子类,因此可以调用适配类的方法,同时实现目标接口的方法。例如,假设我们有一个旧的类库,其中有一个类叫做OldClass,它有一个方法oldMethod()。然而,我们的系统需要一个新的接口NewInterface,其中定义了一个方法newMethod()。通过类适配器模式,我们可以创建一个适配器类Adapter,它既继承OldClass,又实现NewInterface。这样,Adapter类就可以通过调用OldClass的oldMethod()方法来实现NewInterface的newMethod()方法,从而实现接口的转换。2.2 实现方式在类适配器模式中,适配器类通过继承需要适配的类来实现对原有功能的复用,同时通过实现目标接口来提供新的接口方法。这种继承关系使得适配器类可以调用被适配类的方法,并将其转换为目标接口所期望的方法。例如,在Java中,我们可以这样实现一个类适配器:// 目标接口public interface Target { void request();} // 需要适配的类public class Adaptee { public void specificRequest() { // 具体的业务逻辑 }} // 适配器类,继承Adaptee并实现Target接口public class Adapter extends Adaptee implements Target { @Override public void request() { super.specificRequest(); // 调用被适配类的方法 }}在上述代码中,Adapter类通过继承Adaptee类并实现Target接口,将Adaptee类的specificRequest()方法转换为Target接口的request()方法。这样,客户端可以通过调用Adapter类的request()方法来使用Adaptee类的功能,从而实现接口的适配。2.3 优缺点类适配器模式的优点在于简单直接,不需要额外的对象创建开销。然而,它也有一些明显的缺点。首先,Java等语言不支持多继承,这限制了类适配器的使用场景。其次,如果适配的类有很多方法,可能会导致适配器类过于庞大和复杂。优点:● 实现简单,直接通过继承实现接口转换。● 性能开销小,不需要创建额外的对象————————————————原文链接:https://blog.csdn.net/le_duoduo/article/details/145621215
-
作为一名Java工程师,效率就是生产力。那些能让你少写代码、少改BUG、少加班的工具,往往能为你节省大量时间,让你专注于解决真正有挑战性的问题。下面分享的这些工具几乎覆盖了Java开发全流程,从编码、调试到构建、部署,每一个环节都能大幅提升你的工作效率。(文末彩蛋,必得!)一、IDE增强类工具1. IntelliJ IDEA终极版 + 精选插件作为Java开发的首选IDE,IntelliJ IDEA本身已经非常强大,但配合以下插件,效率可以再提升一个档次:• Key Promoter X: 显示你手动操作的快捷键,帮助你养成使用快捷键的习惯• AiXcoder Code Completer: 基于AI的代码补全,比IDEA自带的更智能• Maven Helper: 解决Maven依赖冲突的神器• Lombok: 减少模板代码编写• Rainbow Brackets: 彩色括号,让嵌套结构一目了然实用技巧:创建多个Live Templates(代码模板),比如定义日志、常用异常处理、单例模式等。每天能节省几十次重复输入。2. Lombok虽然这是一个库,但它堪称效率工具。通过注解的方式,自动生成getter/setter、构造函数、equals/hashCode等方法,大幅减少模板代码量。@Data@Builder@NoArgsConstructor@AllArgsConstructorpublic class UserDTO { private Long id; private String username; private String email; // 无需编写getter/setter/构造函数/toString等}AI生成项目注意事项:使用@EqualsAndHashCode时,注意排除可能造成循环引用的字段;使用@Builder时,考虑添加@NoArgsConstructor满足序列化需求。二、调试与性能分析工具3. Arthas阿里开源的Java诊断工具,它能在线排查问题,无需重启应用。最强大的是它能够实时观察方法的入参、返回值,统计方法执行耗时,甚至动态修改类的行为。常用命令:• watch 监控方法调用• trace 跟踪方法调用链路• jad 反编译类• sc 查找加载的类• redefine 热更新类实战示例:线上问题排查,不方便加日志时,用watch命令观察方法执行:watch com.example.service.UserService queryUser "{params,returnObj}" -x 3AI生成项目4. JProfilerJava剖析工具的王者,能够分析CPU热点、内存泄漏、线程阻塞等问题。与其他分析工具相比,JProfiler的UI更友好,数据呈现更直观。核心功能:• 内存视图:找出占用内存最多的对象• CPU视图:定位热点方法• 线程视图:发现死锁和阻塞• 实时遥测:监控线上应用,无需重启技巧:养成定期对自己负责的服务做性能分析的习惯,很多问题在上线前就能发现。5. Charles/Fiddler抓包工具是API调试的必备利器。Charles(Mac)或Fiddler(Windows)能够拦截、查看和修改HTTP/HTTPS请求和响应。实用功能:• 模拟网络延迟• 请求重写• 断点调试HTTP请求• 反向代理在前后端分离开发和调试第三方API时,这类工具能节省大量时间。三、代码质量工具6. SonarQube + SonarLintSonarQube是静态代码分析工具,可以检测代码中的漏洞、坏味道和潜在bug。而SonarLint是其IDE插件版,能在你编码时实时提供反馈。最佳实践:• 在CI流程中集成SonarQube• 为团队制定"质量门"标准• 使用SonarLint实时检查,避免代码审查时返工技巧:自定义规则集,忽略对特定项目不适用的规则,避免"过度洁癖"。7. ArchUnit用代码的方式测试架构规则,确保项目架构不会随着时间推移而腐化。@Testpublic void servicesAndRepositoriesShouldNotDependOnControllers() { ArchRule rule = noClasses() .that().resideInAPackage("..service..") .or().resideInAPackage("..repository..") .should().dependOnClassesThat().resideInAPackage("..controller.."); rule.check(importedClasses);}AI生成项目将架构约束加入单元测试,比写文档更有效,因为违反规则会导致测试失败。8. JaCoCo代码覆盖率工具,与Maven/Gradle集成,生成直观的HTML报告。它不仅统计单元测试覆盖了哪些代码,还能显示哪些分支没有测试到。实用配置:在Maven中设置覆盖率阈值,低于阈值则构建失败:<configuration> <rules> <rule> <element>BUNDLE</element> <limits> <limit> <counter>LINE</counter> <value>COVEREDRATIO</value> <minimum>0.80</minimum> </limit> </limits> </rule> </rules></configuration>AI生成项目四、API开发与测试工具9. Postman + NewmanPostman是API开发和测试的标准工具,而Newman是其命令行版本,适合集成到CI/CD流程中。高级用法:• 环境变量管理不同测试环境• 请求前/后脚本自动化测试• 导出集合到Newman在CI中执行• 团队共享API集合技巧:为每个项目创建环境变量集合,包含测试环境、开发环境、生产环境配置,一键切换。10. OpenAPI Generator从OpenAPI(Swagger)规范自动生成API客户端和服务器端代码。openapi-generator generate -i swagger.json -g spring -o my-spring-serverAI生成项目前后端并行开发时,通过API优先设计,让前端可以基于Swagger UI与Mock服务器工作,而后端则基于生成的接口实现业务逻辑。五、数据库工具11. DBeaver全能型数据库客户端,支持几乎所有主流数据库,功能强大且开源免费。必备功能:• ER图可视化• 数据导出/导入• SQL格式化• 数据库比较• 执行计划分析技巧:使用其"SQL模板"功能,保存常用查询模板,提高重复查询效率。12. Flyway/Liquibase数据库版本控制工具,将数据库结构变更纳入版本管理,确保开发、测试和生产环境的数据库结构一致性。以Flyway为例:@Beanpublic Flyway flyway() { return Flyway.configure() .dataSource(dataSource) .locations("classpath:db/migration") .load();}AI生成项目最佳实践:• 每个变更一个脚本文件• 脚本文件命名规范化• 脚本必须是幂等的• 将验证步骤集成到CI流程六、构建与部署工具13. Gradle + Kotlin DSL虽然Maven仍是Java构建工具的主流,但Gradle的灵活性和性能优势明显。使用Kotlin DSL而非Groovy可以获得更好的IDE支持和类型安全。plugins { id("org.springframework.boot") version "2.7.0" id("io.spring.dependency-management") version "1.0.11.RELEASE" kotlin("jvm") version "1.6.21"} dependencies { implementation("org.springframework.boot:spring-boot-starter-web") testImplementation("org.springframework.boot:spring-boot-starter-test")}AI生成项目优势:• 增量构建更快• 依赖缓存更智能• 自定义任务更灵活• 多项目构建更高效14. Docker + Docker Compose容器化是现代Java开发的标配,Docker让环境一致性问题成为历史。实用命令:# 启动开发环境所需的所有服务docker-compose up -d# 查看容器日志docker logs -f container_name# 进入容器内部docker exec -it container_name bashAI生成项目技巧:创建一个包含常用中间件(MySQL、Redis、RabbitMQ等)的docker-compose.yml,一键启动开发环境。15. GitHub Actions/JenkinsCI/CD是提高团队效率的关键环节。GitHub Actions适合开源项目,Jenkins则更适合企业内部构建流程。GitHub Actions示例:name: Java CI on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up JDK 17 uses: actions/setup-java@v2 with: java-version: '17' distribution: 'adopt' - name: Build with Gradle run: ./gradlew buildAI生成项目最佳实践:将代码风格检查、单元测试、集成测试、安全扫描全部纳入CI流程,确保代码质量。七、辅助工具16. PlantUML用代码生成UML图,比拖拽式画图工具更高效,特别是需要频繁修改图表时。可以和版本控制系统无缝集成。@startumlpackage "Customer Domain" { class Customer class Address Customer "1" *-- "n" Address}package "Order Domain" { class Order class LineItem Order "1" *-- "n" LineItem Order "*" -- "1" Customer}@endumlAI生成项目IDEA集成:安装PlantUML插件,编写代码时实时预览图表。17. Obsidian/Logseq知识管理工具,基于Markdown文件的本地知识库。对于需要持续学习的Java工程师来说,构建个人知识体系至关重要。推荐用法:• 每学习一个新技术,创建一个页面• 记录常见错误和解决方案• 构建项目文档和架构决策记录• 使用日常笔记捕捉想法和灵感技巧:利用双向链接功能,将知识点相互关联,构建知识网络,而非简单的知识树。18. Claude Code 国内直接使用的 Claude code 接口,注册直接有 100美金额度。可以使用 claude-opus-4、claude-sonnet-4..————————————————原文链接:https://blog.csdn.net/kujie0121/article/details/149267402
上滑加载中
推荐直播
-
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步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签