-
1.概念1.1.集合与数组的区别集合:长度不固定,动态的根据数据添加删除改变长度,并且只能存入引用类型,读取采用迭代器或其他方法数组:长度固定,不可改变,既可以存入基本类型也可以存入引用类型,读取使用索引读(for)长度 存入类型 读取集合 长度不固定,动态的根据数据添加删除改变长度 只能存入引用类型 采用迭代器或其他方法数组 长度固定,不可改变 既可以存入基本类型也可以存入引用类型 使用索引(for)1.2.集合分类分为三类:List类,Set类,Map类List集合:集合里面元素有序,并且允许可重复Set集合:集合里面元素无序,并且不可重复(保证唯一性)Map集合:集合采用键值对方式,key唯一(不允许重复)无序,value没有要求是否有序 是否可重复List 有序 可重复Set 无序 不可重复1.3.Collection和Collections的区别Collection是一个接口,给集合实现的,里面定义了一些操作集合的方法Collections是一个工具类,位于java.util包中,可以直接使用该类操作集合(增删改,排序)1.4.集合遍历的方法有六种方法:for,增强for,迭代器,列表迭代器,foeEach,Stream流for:带索引查询(区分集合是否带索引,才能使用该方法)List<String> list = Arrays.asList("A", "B", "C"); // 通过索引遍历(适合 ArrayList 等支持随机访问的集合)for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i));}增强for:没有索引,直接遍历查询List<String> list = Arrays.asList("A", "B", "C"); // 直接遍历元素(底层基于迭代器实现)for (String item : list) { System.out.println(item);}迭代器:在迭代器里面只能删除元素,不能插入元素List<String> list = Arrays.asList("A", "B", "C");Iterator<String> iterator = list.iterator(); // 通过迭代器遍历(适用于所有 Collection)while (iterator.hasNext()) { String item = iterator.next(); System.out.println(item); // 可在遍历中安全删除元素:iterator.remove();}List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String item = iterator.next(); if ("B".equals(item)) { iterator.remove(); // 允许删除当前元素 // iterator.add("D"); // 编译错误:Iterator 没有 add() 方法 }}列表迭代器:没有限制,可以进行删除查询插入元素List<String> list = Arrays.asList("A", "B", "C");ListIterator<String> listIterator = list.listIterator(); // 正向遍历(从头到尾)while (listIterator.hasNext()) { String item = listIterator.next(); System.out.println(item);} // 反向遍历(从尾到头)while (listIterator.hasPrevious()) { String item = listIterator.previous(); System.out.println(item);}List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));ListIterator<String> listIterator = list.listIterator(); // 正向遍历while (listIterator.hasNext()) { String item = listIterator.next(); if ("B".equals(item)) { listIterator.remove(); // 删除当前元素 listIterator.add("D"); // 在当前位置插入新元素 listIterator.set("E"); // 替换当前元素(需在 next() 或 previous() 后调用) }} // 反向遍历while (listIterator.hasPrevious()) { String item = listIterator.previous(); System.out.println(item);}forEach:因为它基于迭代器实现的,因此也不能在循环中插入元素List<String> list = Arrays.asList("A", "B", "C"); // 使用 Lambda 表达式遍历list.forEach(item -> System.out.println(item)); // 或使用方法引用list.forEach(System.out::println);Stream:没有限制List<String> list = Arrays.asList("A", "B", "C"); // 转换为 Stream 并遍历list.stream().forEach(item -> System.out.println(item)); // 并行流遍历(多线程处理)list.parallelStream().forEach(item -> System.out.println(item));2.List2.1.List的实现实现List的集合有:ArrayList,LinkedList,VectorArrayList:基于动态的数组创建的,查询效率高,增删效率一般,线程不安全LinkedList:基于双向链表创建的,查询效率一般,增删效率高,线程不安全Vector:基于动态数组创建的,与ArrayList类似,不过它是线程安全的数据结构 读操作 写操作 线程安全ArrayList 数组 高 一般 不安全LinkedList 双向链表 一般 高 不安全Vector 数组 高 一般 安全2.2.可以一边遍历一边修改List的方法首先思考有几个遍历方法:六个哪些是不能修改元素的:迭代器,forEach最终得到的方法:for,增强for,列表迭代器,Stream流2.3.List快速删除元素的原理原理是基于集合底层数据结构不同,分为两类:ArrayList,LinkedListArrayList:基于数组对吧,原先数组是通过索引删除数据,那么因此ArrayList也是如此,基于索引来删除数据具体实现:如果你是删除尾部最后一个数据,直接删除即可,时间复杂度为O(1),如果不是,那么它会将索引元素删除后,将后面的元素往前面覆盖,然后计算出集合长度,时间复杂度为O(n),n为元素的个数LinkedList:基于双向链表,简单来说链表由节点组成,每个节点包含自己的数据与前一个节点的引用和后一个节点的引用,实现双向并通具体实现:如果你是删除尾部最后一个数据,直接删除即可,时间复杂度为O(1),如果不是,那么就是从头或尾进行查询删除,时间复杂度O(n)2.4.ArrayList与LinkedList的区别数据结构组成不同:Array List基于数组,LinkedList基于双向链表删除和插入效率不同:ArrayList在尾部的效率高(平均O(1)),在其他的地方效率低,由于需要进行元素覆盖,而LinkedList它基于链表引用,在尾部的效率(O(1)比ArrayList效率低(ArrayList基于数组,内存是连续的,而LinkedList基于链表,内存不连续),在其他地方删除与插入与ArrayList效率差不多(O(n))随机访问速度:由于ArrayList基于数组根据索引查询,时间复杂度O(1),而LinkedList基于链表,它需要从头或尾部访问,因此时间复杂度为O(n)适用场景不同:ArrayList更适合高频的随机访问操作或尾部插入为主,LinkedList更适合高频头尾插入/删除(队列)或需要双向遍历线程安全:都是线程不安全的2.5.线程安全实现线程(List)安全的方法有:实现Collections.synchronizedList,将线程不安全的List集合加个锁,变成安全的直接使用线程安全的List集合:比如Vector,CopyOnWirteArrayList2.6.ArrayList的扩容机制首先如果你没有指定长度,默认长度为10,当你要添加元素并且超过此时容量长度时,就会进行扩容操作实现:1.扩容:创建一个新的数组,新数组的长度为原数组的1.5倍数,然后再检查容量是否足够,不够继续扩容---2.复制:将旧的数组里面的值复制进新的数组中,再进行写操作---3.更改引用:将原先指向旧数组的引用指向新数组---4.扩容完成:可以继续扩容2.7.CopyOnWirteArrayList它实现了读写分离,写操作加了互斥锁ReentrantLock,避免出现线程安全问题,而读操作没有加锁,使用volatile关键字修饰数组,保证当前线程对数组对象重新赋值后,其他线程可以及时感知到(所有线程可见性)。线程读取数据可以直接读取,提高效率写操作:它不会向ArrayList一样直接扩容1.5倍,它是根据你的添加元素个数多少来扩容,如果你只添加一个元素,那么它会创建一个新数组,长度比旧数组长度多一,然后依旧是依次复制元素进新数组中,改变内部引用指向(需要频繁创建新的数组,以时间换空间)读操作:就是说它不会管你的数据是否修改,内部指向是旧数组,那么就读取旧数组的数据,指向是新数组就读取新数据,这样效率会高(数据弱一致性)————————————————原文链接:https://blog.csdn.net/2402_88700528/article/details/148262923
-
在Java中,==和equals()是两个常用的比较操作符和方法,但它们之间的用法和含义却有着本质的区别。本文将详细解释这两个操作符/方法之间的区别。1、==操作符==操作符 在Java中 主要用于比较两个变量的值是否相等。但是,这个“值”的含义取决于变量的类型:1、于基本数据类型(如int, char, boolean等):== 比较的是两个变量的值是否相等。2、对于引用类型(如对象、数组等):== 比较的是两个引用是否指向内存中的同一个对象(即地址是否相同)。示例:int a = 5; int b = 5; System.out.println(a == b); // 输出true,因为a和b的值相等 Integer c = new Integer(5); Integer d = new Integer(5); System.out.println(c == d); // 输出false,因为c和d指向的是不同的对象2、equals()方法equals()方法是Java Object 类的一个方法,用于比较两个对象的内容是否相等。需要注意的是,默认的 equals() 方法 实现其实就是 == 操作符对于引用类型的比较,即比较的是两个引用是否指向同一个对象。但是,很多Java类(如String, Integer等)都重写了 equals() 方法,以提供基于内容的比较。示例:String str1 = new String("hello"); String str2 = new String("hello"); System.out.println(str1.equals(str2)); // 输出true,因为str1和str2的内容相等 // Integer类也重写了equals方法 Integer e = new Integer(5); Integer f = new Integer(5); System.out.println(e.equals(f)); // 输出true,因为Integer类重写了equals方法,基于值进行比较3、总结1、==操作符:对于基本数据类型,比较的是值是否相等。对于引用类型,比较的是两个引用是否指向同一个对象(即地址是否相同)。2、equals()方法:默认实现是基于 == 操作符的,即比较两个引用是否指向同一个对象。但很多类(如String, Integer等)都重写了 equals() 方法,以提供基于内容的比较。重要提示:1、当比较两个对象的内容是否相等时,应该优先使用 equals() 方法,而不是 == 操作符。2、自定义类如果需要比较内容是否相等,也应该重写 equals() 方法。3、需要注意的是,如果重写了 equals() 方法,通常也需要重写 hashCode() 方法,以保持两者的一致性。这是因为在Java中,很多集合类(如HashSet, HashMap等)在存储和查找元素时,都会同时用到 equals() 和 hashCode() 方法。————————————————原文链接:https://blog.csdn.net/qq_41840843/article/details/139706184
-
1.内存模型1.1.JVM内存模型的介绍内存模型主要分为五个部分:虚拟机栈,本地方法栈,堆,方法区(永久代或元空间),程序计数器,当然还有一部分是直接内存。虚拟机栈:每个线程各有一个,线程独有,当执行方法(除了本地方法native修饰的方法)之前,会创建一个栈帧,栈帧里面包含局部变量表和操作数栈和动态链接和方法出口等信息,而每个栈帧就是存入栈中本地方法栈:每个线程各有一个,线程独有,当执行本地方法时类似于虚拟机栈,一样会创建栈帧,存入对应信息程序计数器:每个线程各有一个,线程独有,它的作用是记录当前线程下一次执行的二进制字节码指令地址,如果执行的是本地方法那么它会记录为定值(null)堆:所有线程共享,堆的回收由垃圾回收机制管理,堆中主要存入对象实例信息,类信息,数组信息,堆是JVM内存中最大的一个永久代:在jdk1.7及以前是方法区的实现,使用的是jvm内存,独立于堆,主要存入类信息,静态变量信息,符号引用等信息元空间:在jdk1.8及以后是方法区的实现,使用的是本地内存,主要存入类信息,静态变量信息,符号引用等信息直接内存:该内存属于操作系统,由NIO引入,操作系统和Java程序都可以进行操作,实现共享----常量池:属于class文件的一部分,主要存储字面量,符号引用----运行时常量池:属于方法区,其实就是将常量池中的符号引用替换成了直接引用,其余一样1.2.堆和栈的区别五个点:用途,生命周期,存储速度,存储空间,可见性用途:栈主要存储方法返回地址,方法参数,临时变量,每次方法执行之前会创建栈帧,而堆存储对象的实例信息,类实例信息,数组信息生命周期:栈的生命周期可见,每次方法执行完栈帧就会移除(弹出),而堆中的数据需要由垃圾回收器回收,回收时间不确定存储速度:栈的速度更快,栈保持"先进后出"的原则,操作简单快,而堆需要对对象进行内存分配和垃圾回收,并且垃圾回收器本身运行也会损耗性能,速度慢存储空间:栈的空间相对于堆的空间小,栈的空间小且固定,由操作系统管理,而堆的空间是jvm中最大的,由jvm管理可见性:栈是每个线程都有的,而堆是所有线程共享的1.3.栈的存储细节如果执行方法时,里面创建了基本类型,那么基本类型的数据会存入栈中,如果创建了引用类型,会将地址存入栈,其实例数据存入堆中1.4.堆的部分堆主要分为两部分:新生代,老年代,它的比例:1:2新生代:新生代分为两个区:伊甸园区和幸存者区,而幸存者区又平均分为S0和S1区,伊甸园区与S0与S1之间的比例:8:1:1,每次新创建的对象实例都会先存入伊甸园区,它们主要使用的垃圾回收算法是复制算法,当伊甸园区的内存使用完时,会使用可达性分析算法,标记不可存活的对象(没有被引用的对象)将存活对象复制移入S0或S1中,这个过程叫Minor GC,如果这次移入的是S0,那么下次就会将伊甸园区和S0中的对象移入S1中,循环反复,每经历一次Minor GC过程就会给对象年龄加一,直到大于等于15时,会认为该对象生命周期长,移入老年代中细节:其实新创建的对象不会直接存入伊甸园区,如果多线程情况下同时进行存入对象(线程竞争压力大)会导致性能的损失,因此会给每个线程从伊甸园区中先申请一块TLAB区域,先将对象存入该区,如果该区内存使用完,会重写申请或直接存入伊甸园区老年代:老年代就是存储生命周期长的对象(不经常回收的对象),主要使用的垃圾回收算法为标记清除算法或标记整理算法,看场景出发,其中老年代还包含一个大对象区大对象区:主要存储的就是新创建的大对象比如说大数组,会直接将该对象存入大对象区中,不在存入新生代可达性分析算法:从GC Root出发找对应引用对象,如果一个对象没有被直接引用或间接引用,那么会被标记,GC Root可以是java的核心库中的类,本地方法使用的类,还未结束的线程使用的类,使用了锁的类标记清除算法:对引用的对象进行标记,然后进行清除(不是真正的清除,而是记录其对象的起始地址和结束地址到一个地址表中,下次要添加新对象时会先从表中找,找到一个适合大小的就会进行覆盖),清除:记录地址,新对象进行覆盖,好处:速度快,缺点:内存碎片化严重(内存不连续了,本来可以存入的对象存入不了)标记整理算法:同理进行标记,然后再对可存活对象进行整理,最后清除,好处:避免了内存碎片化问题,缺点:速度慢复制算法:将内存空间分为两份,一份存对象from,一份为空to,当要回收时,复制可存活对象移入为空的内存空间to中(移入既整理),然后对存对象的空间from整体清除,然后名称from和to换过来为什么会有大对象区:因为伊甸园区的内存空间本身就不大,如果你直接创建一个大于它空间的对象,会出现问题,还有就是即使没有超过伊甸园区的空间,但是其对象依旧很大,频繁的复制移动很影响性能1.5.程序计数器的作用简单来说:线程1执行到某个地方时,线程2抢到了执行权,那么等到线程1执行时是不是需要知道上次执行到哪里了,所以程序计数器就是记录执行到哪里的,并且每次线程都需要有一个来记录1.6.方法区的内容方法区主要包含:类信息,静态变量信息,运行时常量池,即时编译器的缓存数据1.7.字符串池在jdk1.6及以前字符串池属于永久代,jdk1.7字符串池移入堆中但是还是属于永久代的,jdk1.8及以后还是存入堆中,但是不属于元空间了(1.7以前是永久代,1.8以后是元空间)细节:String s1 = "a";它的过程是:先去字符串池中找,看是否能找到该字符,找到了直接复用池中地址,没有找到会先在堆中创建一个String对象,jdk1.6它会将数据复制一份重新创建一个新的对象存入池中,jdk1.7会将其地址复用给池中String s2 = new("b");同理String s3 = "a" + "b";常量进行相加,与new("ab")基本一致String s4 = s1 + s2;变量相加,底层使用的是new StringBuilder.append("a").append("b").toString(),如果池中存在"ab",它也不会复用,而是直接创建,如果池中不存在,而不会将新创建的对象存入池中1.8.引用类型引用类型:强引用,软引用,弱引用,虚引用,(终结器引用)强引用:比如new就是,只要有强引用指向对象,那么该对象永远不会被回收软引用:如果出现内存溢出的情况,再下次GC时会对其回收弱引用:每次进行GC过程都会进行回收虚引用:每次进行GC过程都会进行回收细节:这些都是对象,等级依次递减软引用:创建一个软引用对象时你可以指定引用队列,如果不指定会导致软引用为null一个空壳,比如说出现了GC Root强引用软引用对象,导致软引用对象无法被回收,你想要其对象被回收,可以使用引用队列,简单来说就是出现了这种情况,将软引用对象存入队列中,下次GC会扫描队列进行回收,当然这是特殊情况,总结来说:软引用可以使用引用队列也可以不使用public class SoftRefDemo { public static void main(String[] args) throws InterruptedException { // 1. 创建引用队列 ReferenceQueue<Object> queue = new ReferenceQueue<>(); // 2. 创建大对象(确保能被GC回收) byte[] data = new byte[10 * 1024 * 1024]; // 10MB // 3. 创建软引用并关联队列 SoftReference<Object> softRef = new SoftReference<>(data, queue); // 4. 移除强引用(只保留软引用) data = null; System.out.println("GC前: "); System.out.println(" softRef.get() = " + softRef.get()); System.out.println(" queue.poll() = " + queue.poll()); // 5. 强制GC(模拟内存不足) System.gc(); Thread.sleep(1000); // 给GC时间 System.out.println("\nGC后: "); System.out.println(" softRef.get() = " + softRef.get()); System.out.println(" queue.poll() = " + queue.poll()); }}GC前: softRef.get() = [B@15db9742 queue.poll() = null GC后: softRef.get() = null queue.poll() = java.lang.ref.SoftReference@6d06d69c弱引用:与软引用相同WeakHashMap<Key, Value> map = new WeakHashMap<>(); Key key = new Key();map.put(key, new Value()); // 移除强引用key = null; System.gc(); // GC后Entry自动被移除System.out.println(map.size()); // 输出: 0虚引用:最好的例子就是直接内存:它就是使用了虚引用,直接内存就是从操作系统中申请了一块空间来使用,因此GC是不能对其进行回收的,如果当强引用消失只剩下虚引用,那么会将虚引用对象存入引用队列中,等队列来执行本地方法释放直接内存public class PhantomRefDemo { public static void main(String[] args) { // 1. 创建引用队列 ReferenceQueue<Object> queue = new ReferenceQueue<>(); // 2. 创建虚引用 Object obj = new Object(); PhantomReference<Object> phantomRef = new PhantomReference<>(obj, queue); // 3. 模拟直接内存分配 ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024 * 1024); // 1MB System.out.println("GC前:"); System.out.println(" phantomRef.get() = " + phantomRef.get()); // null System.out.println(" queue.poll() = " + queue.poll()); // null // 4. 移除强引用(触发回收条件) obj = null; directBuffer = null; // 释放DirectByteBuffer强引用 // 5. 强制GC(实际应用中会自动触发) System.gc(); try { Thread.sleep(500); } catch (Exception e) {} System.out.println("\nGC后:"); System.out.println(" phantomRef.get() = " + phantomRef.get()); // null System.out.println(" queue.poll() = " + queue.poll()); // 返回phantomRef对象 // 6. 实际效果:DirectByteBuffer分配的1MB堆外内存已被释放 }}终结器引用:在所有父类Object中有一个终结器方法finalize()方法,如果重写该方法,那么执行GC之前会先执行该方法,当没强引用指向了,而这个对象还重写了finalize()方法,那么会将这个终结器引用对象加入队列中,下次GC时会先由队列来执行finalize()方法,但是指定执行的队列是一个优先级不高的队列,会导致资源释放缓慢public class ResourceHolder { // 重写finalize方法(不推荐!) @Override protected void finalize() throws Throwable { releaseResources(); // 释放资源 super.finalize(); }}1.9.内存泄漏与内存溢出内存泄漏:就是说没有被引用的对象没有被回收,导致可用内存空间减少比如:静态集合没有释放:一直存在线程未释放:线程应该执行完了,但是没有释放事件监听:事件源都不存在了,还在监听例子:使用对应的文件流,字节流,但是没有释放该流,就会导致内存泄漏解决:释放流内存溢出:就是说内存不足了比如:一直创建新对象持久引用:集合一直添加但是没有被清除递归例子:ThreadLocal,每个线程都有一个ThreadLocal,本质就是每个线程存在一个ThreadLocalMap对象,key(弱引用)存入的是TreadLocal的实例,value(强引用)为自己指定的Object对象,如果没有使用该TreadLocal了,也就是说没有强引用指向TreadLocalMap对象,那么其中的key就会被设置为null,那如果该线程一直不结束,导致key不能被回收,随着key为null的情况增多就会导致内存溢出解决:使用TreadLocal.recome();1.10.会出现内存溢出的结构会出现该问题的内存结构:堆,栈,元空间,直接空间————————————————原文链接:https://blog.csdn.net/2402_88700528/article/details/148516238
-
1. Canvas 基础概念什么是 Canvas?HTML5 提供了 canvas元素,这是一个空白的矩形区域,可以使用 JavaScript 在上面绘制图形、图像和文本<canvas id="myCanvas" width="500" height="300"></canvas>获取 Canvas 绘图上下文要在 Canvas 上绘图,首先需要获取绘图上下文(context)。Canvas 支持不同的绘图上下文类型:2D 上下文 (CanvasRenderingContext2D):用于绘制 2D 图形,支持路径绘制、填充、描边、文本绘制、图像处理等。WebGL (WebGLRenderingContext):用于 3D 图形渲染,基于 OpenGL ES。示例:获取 2D 上下文<canvas id="myCanvas" width="500" height="300"></canvas><script> const canvas = document.getElementById("myCanvas"); const ctx = canvas.getContext("2d"); // 获取 2D 绘图上下文</script>2. 基本绘图操作绘制矩形ctx.fillStyle = "red"; // 设置填充颜色ctx.fillRect(50, 50, 100, 100); // 绘制填充矩形ctx.strokeStyle = "blue"; // 设置描边颜色ctx.strokeRect(200, 50, 100, 100); // 绘制描边矩形效果绘制路径(线段)ctx.beginPath(); // 开始路径ctx.moveTo(50, 200); // 起点ctx.lineTo(150, 250); // 第一条线ctx.lineTo(250, 200); // 第二条线ctx.closePath(); // 关闭路径ctx.stroke(); // 绘制路径 效果绘制圆形ctx.beginPath();ctx.arc(150, 150, 50, 0, Math.PI * 2); // 绘制一个圆弧路径ctx.fill(); // 填充路径ctx.arc(150, 150, 50, 0, Math.PI * 2)绘制一个圆弧路径。150, 150:圆心的坐标(x, y)。50:圆的半径。0:起始角度(弧度制),0表示从3点钟方向开始。Math.PI * 2:结束角度,Math.PI * 2表示完整的360度,即一个完整的圆。效果3. 图像与文本绘制绘制图片Canvas 可以绘制图片,通过 drawImage 方法将图片绘制到 Canvas 上const canvas = document.getElementById('myCanvas');const ctx = canvas.getContext('2d');const img = new Image();img.src = 'http://gips1.baidu.com/it/u=1658389554,617110073&fm=3028&app=3028&f=JPEG&fmt=auto'; // 替换为实际图像 URLimg.onload = () => { // 绘制原始大小 ctx.drawImage(img, 0, 0); // 绘制缩放后的图片 ctx.drawImage(img, 50, 50, 128, 96); // (image, x, y, width, height)}; 效果绘制文本通过 fillText 和 strokeText 方法,可以在 Canvas 上绘制文本。const canvas = document.getElementById('myCanvas');const ctx = canvas.getContext('2d');// 设置字体ctx.font = "30px Arial";ctx.fillStyle = '#FF0000';ctx.fillText("Hello, Canvas!", 50, 50);ctx.strokeText("Outlined Text", 50, 100);效果4. Canvas 动画基础动画实现Canvas 动画通常通过 requestAnimationFrame 进行帧更新。在 requestAnimationFrame 周期内,通过清除先前的 Canvas 内容并绘制新内容,可以实现流畅的动画效果const canvas = document.getElementById('myCanvas');const ctx = canvas.getContext('2d');let posX = 0;const posY = 150;function animate() { // 清除画布 ctx.clearRect(0, 0, canvas.width, canvas.height); // 绘制移动的方块 ctx.fillStyle = '#FF5733'; ctx.fillRect(posX, posY, 50, 50); // 更新位置 posX += 2; // 重置位置 if (posX > canvas.width) posX = -50; // 循环调用 requestAnimationFrame(animate);}animate(); // 启动动画5. Canvas 性能优化在使用 Canvas 进行绘图和动画时,性能优化尤为重要,尤其是在处理复杂图形和高频率动画时1. 减少重绘次数尽量只更新变化部分,避免整屏重绘。使用 requestAnimationFrame 来协调动画更新,避免不必要的渲染。function draw() { // 仅在需要时重绘 if (needsRedraw) { ctx.clearRect(0, 0, canvas.width, canvas.height); // 执行绘图操作 needsRedraw = false; } requestAnimationFrame(draw);}2. 使用离屏 Canvas利用离屏 Canvas 进行预绘,然后将结果绘制到主 Canvas 上,减少主线程的计算压力,提升渲染效率// 创建离屏 Canvasconst offscreenCanvas = document.createElement("canvas");const offCtx = offscreenCanvas.getContext("2d");// 在离屏 Canvas 上绘制offCtx.fillStyle = 'red';offCtx.fillRect(0, 0, 100, 100);// 将离屏 Canvas 的内容绘制到主 Canvasconst canvas = document.getElementById('myCanvas');const ctx = canvas.getContext('2d');ctx.drawImage(offscreenCanvas, 50, 50);3. 使用多个层 Canvas将复杂的静态背景和动态元素分层绘制,可以减少需要频繁更新的绘图区域,提升整体渲染效率<div style="position: relative;"> <!-- 静态背景 Canvas --> <canvas id="bgCanvas" width="600" height="400" style="position: absolute; z-index: 0;"></canvas> <!-- 动态元素 Canvas --> <canvas id="fgCanvas" width="600" height="400" style="position: absolute; z-index: 1;"></canvas></div><script> const bgCanvas = document.getElementById('bgCanvas'); const bgCtx = bgCanvas.getContext('2d'); // 绘制静态背景 bgCtx.fillStyle = '#EEEEEE'; bgCtx.fillRect(0, 0, bgCanvas.width, bgCanvas.height); const fgCanvas = document.getElementById('fgCanvas'); const fgCtx = fgCanvas.getContext('2d'); let posX = 0; const posY = 200; function animate() { fgCtx.clearRect(0, 0, fgCanvas.width, fgCanvas.height); fgCtx.fillStyle = '#FF0000'; fgCtx.fillRect(posX, posY, 50, 50); posX += 2; if (posX > fgCanvas.width) posX = -50; requestAnimationFrame(animate); } animate();</script>6. Canvas VS SVGSVG(Scalable Vector Graphics)是一种用来描述二维矢量图形的XML格式。它是一种基于文本的图像格式,支持交互和动画,广泛用于网页设计和开发中。在选择 Canvas 或 SVG 技术进行项目开发时,需要考虑到它们各自的特性、适用场景和项目需求Canvas 是基于像素的技术,适用于生成即时图形,在处理大量对象(如游戏中的图形)时,Canvas 通常表现得更好,因为它直接操作位图。但每次绘制都是对整个 Canvas 的再绘制,无法直接对单个元素进行操作SVG 是矢量图形技术,缩放时不会失去清晰度,适合高保真度的图像展示,每个元素都是独立可操作的 DOM 元素,可以添加事件、样式和脚本,但对于复杂图形,DOM 节点数可能较大特性 Canvas SVG绘制方式 基于像素的位图绘制 基于矢量的图形绘制适用场景 高性能实时渲染、大量动态图形、游戏等 需要可缩放、交互性高的静态图形、复杂的布局与样式DOM 结构 单一 <canvas>,绘图内容不在 DOM 中 每个元素都是 DOM 节点,易于操作和样式化性能 适合大量图形和高频率更新,性能较高 动态元素较多时性能可能下降,特别是复杂的 SVG 图形可访问性 需要额外处理,默认不可访问 元素可被屏幕阅读器等辅助技术识别,具备更好的可访问性可缩放性 失真 适用于缩放动画与交互 需要手动实现动画和交互逻辑 支持 CSS 动画、SVG 动画和事件处理如何选择?动态动画(游戏、数据可视化)➡ Canvas可缩放的矢量图(图标、交互 UI)➡ SVG本文提供了 Canvas 的核心知识点、基础绘图、动画处理及优化技巧。希望对你有所帮助!————————————————原文链接:https://blog.csdn.net/XH_jing/article/details/146223643
-
一、var 声明(一)定义与基本用法var 是 JavaScript 中较早期用于声明变量的关键字。使用 var 声明变量非常简单,只需要在变量名前加上 var 关键字即可。例如:var age; age = 25; // 或者可以在声明时直接赋值 var name = "John";(二)作用域var 声明的变量具有函数作用域或全局作用域。这意味着在函数内部使用 var 声明的变量,在整个函数内部都是可访问的,但在函数外部无法访问。例如:function exampleFunction() { var localVar = "I'm a local variable"; console.log(localVar); // 输出: I'm a local variable } console.log(localVar); // 这里会报错,因为 localVar 在函数外部不可访问如果在全局作用域(即不在任何函数内部)使用 var 声明变量,该变量将成为全局对象(在浏览器环境中是 window,在 Node.js 环境中是 global)的属性。例如:var globalVar = "I'm a global variable"; console.log(window.globalVar); // 输出: I'm a global variable(三)声明提升var 声明存在一个重要特性 —— 声明提升。这意味着在函数或全局作用域内,无论 var 声明的变量出现在何处,其声明都会被提升到作用域的顶部,但是赋值操作不会被提升。例如:console.log(num); // 输出: undefined var num = 10; //上述代码等价于: var num; console.log(num); // 输出: undefined num = 10;这种声明提升可能会导致一些意想不到的结果,特别是在代码结构复杂时。例如:function hoistingExample() { console.log(x); // 输出: undefined if (false) { var x = 10; } console.log(x); // 输出: undefined } hoistingExample();在这个例子中,虽然 if 块中的代码不会执行,但由于 var 的声明提升,x 的声明仍然被提升到函数顶部,所以第一次 console.log(x) 输出 undefined。而第二次输出 undefined 是因为 if 块内的赋值操作没有执行。(四)重复声明使用 var 可以对同一个变量进行多次声明,后面的声明会被忽略(但如果有赋值操作,会覆盖之前的值)。例如:var message = "Hello"; var message; console.log(message); // 输出: Hello var count = 5; var count = 10; console.log(count); // 输出: 10二、let 声明(一)定义与基本用法let 是 ES6 引入的用于声明变量的关键字。它的基本用法与 var 类似,在变量名前加上 let 即可声明变量。例如:let age; age = 30; // 或者声明时直接赋值 let name = "Jane";(二)作用域let 声明的变量具有块级作用域。块级作用域由一对花括号 {} 定义,包括 if 语句块、for 循环块、while 循环块等。在块级作用域内使用 let 声明的变量,仅在该块级作用域内有效。例如:if (true) { let localVar = "I'm a block - level local variable"; console.log(localVar); // 输出: I'm a block - level local variable } console.log(localVar); // 这里会报错,因为 localVar 在块外部不可访问与 var 的函数作用域相比,块级作用域更加精细,能更好地控制变量的生命周期和作用范围,减少变量污染全局作用域的风险。(三)不存在声明提升与暂时性死区let 声明不存在像 var 那样的声明提升。在使用 let 声明变量之前访问该变量会导致 ReferenceError 错误,这被称为 “暂时性死区”(TDZ)。在代码执行到 let 声明语句之前,该变量就已经存在于其作用域中了,但处于一种 “不可用” 的状态。只有当执行流到达声明语句时,变量才会被初始化,从而可以正常使用。例如:console.log(age); // 报错: ReferenceError: age is not defined let age = 28;在 let age = 28; 这行代码之前,age 处于暂时性死区,任何对它的访问都会触发错误。暂时性死区的存在,实际上是 JavaScript 引擎在解析代码时的一种机制。当遇到 let 声明时,引擎会在作用域中为该变量创建一个绑定,但此时变量处于未初始化状态。只有执行到声明语句本身时,变量才会被初始化并可以正常使用。这一特性使得开发者在编写代码时,对于变量的声明和使用顺序更加清晰,避免了因变量提升而导致的一些难以调试的问题。(四)不能重复声明在同一作用域内,使用 let 重复声明同一个变量会导致 SyntaxError 错误。例如:let count = 5; let count = 10; // 报错: SyntaxError: Identifier 'count' has already been declared这种限制有助于避免变量声明冲突,使代码更加清晰和可维护。三、const 声明(一)定义与基本用法const 同样是 ES6 引入的关键字,用于声明常量。常量一旦声明,其值就不能再被修改。声明常量的方式与 var 和 let 类似,在常量名前加上 const,并且必须在声明时进行初始化赋值。例如:const PI = 3.14159; const MAX_COUNT = 100;(二)作用域const 声明的常量具有块级作用域,与 let 相同。在块级作用域内声明的常量,仅在该块级作用域内有效。例如:if (true) { const localVar = "I'm a constant in a block"; console.log(localVar); // 输出: I'm a constant in a block } console.log(localVar); // 这里会报错,因为 localVar 在块外部不可访问(三)值的不可变性const 声明的常量值不能被重新赋值。尝试对常量重新赋值会导致 TypeError 错误。例如:const PI = 3.14159; PI = 3.14; // 报错: TypeError: Assignment to constant variable.需要注意的是,对于对象和数组类型的常量,虽然不能重新赋值整个对象或数组,但可以修改其内部属性或元素。例如:const person = { name: "Alice", age: 32 }; person.name = "Bob"; // 合法,对象属性可以修改 console.log(person.name); // 输出: Bob const numbers = [1, 2, 3]; numbers.push(4); // 合法,数组元素可以修改 console.log(numbers); // 输出: [1, 2, 3, 4]如果想要确保对象或数组的内容也不可变,可以使用 Object.freeze() 方法。例如:const frozenPerson = Object.freeze({ name: "Charlie", age: 25 }); frozenPerson.name = "David"; // 虽然不会报错,但实际上属性值并未改变 console.log(frozenPerson.name); // 输出: Charlie(四)不存在声明提升与暂时性死区与 let 一样,const 声明也不存在声明提升。在使用 const 声明常量之前访问该常量会导致 ReferenceError 错误,同样存在暂时性死区。在常量声明语句之前,该常量虽然在作用域中已经有了绑定,但处于未初始化状态,无法被访问和使用。例如:console.log(MAX_COUNT); // 报错: ReferenceError: MAX_COUNT is not defined const MAX_COUNT = 200;当代码执行到 const MAX_COUNT = 200; 时,常量 MAX_COUNT 才被初始化并可以正常使用。这与 let 声明的暂时性死区原理一致,都是为了让代码在变量(常量)的声明和使用上更加规范和可预测。四、var、let 和 const 的区别(一)作用域var:具有函数作用域或全局作用域。在函数内部声明的 var 变量在整个函数内有效,在全局作用域声明的 var 变量成为全局对象的属性。let:具有块级作用域。在块级作用域(如 if 块、for 循环块等)内声明的 let 变量仅在该块内有效,能更好地控制变量的作用范围,减少变量污染。const:同样具有块级作用域,与 let 类似,在声明它的块级作用域内有效。(二)声明提升var:存在声明提升,变量声明会被提升到作用域顶部,但赋值操作不会提升。这可能导致在变量声明之前访问它时得到 undefined 值,从而引发一些不易察觉的错误。let:不存在声明提升,在声明变量之前访问会导致 ReferenceError 错误,存在暂时性死区,使得代码在变量声明之前无法访问该变量,提高了代码的可预测性。const:也不存在声明提升,同样存在暂时性死区,在声明常量之前访问会导致 ReferenceError 错误。(三)可变性var:声明的变量可以被重新赋值,也可以在同一作用域内被重复声明(后面的声明会被忽略,有赋值时会覆盖之前的值)。let:声明的变量可以被重新赋值,但在同一作用域内不能重复声明,避免了变量声明冲突。const:声明的常量不能被重新赋值(对于对象和数组类型,虽然不能重新赋值整个对象或数组,但内部属性和元素可以修改,若要完全禁止修改,可使用 Object.freeze() 方法),并且在声明时必须初始化赋值。(四)使用场景建议var:由于其存在声明提升和函数作用域的特性,可能会导致一些代码理解和维护上的困难。在现代 JavaScript 开发中,var 的使用场景逐渐减少,一般仅在需要兼容非常旧的 JavaScript 环境(不支持 ES6 及以上特性)时才考虑使用。let:适用于需要在块级作用域内声明变量,并且变量值可能会发生变化的场景。例如在 for 循环中声明循环变量,或者在 if 块内声明临时变量等。const:用于声明那些值在整个程序运行过程中不会改变的常量,如数学常量(PI)、配置项(MAX_COUNT)等。对于对象和数组类型的常量,如果希望其内部内容也不可变,可结合 Object.freeze() 使用。 综上所述,var、let 和 const 在 JavaScript 中各自具有独特的特性和适用场景。作为新手开发者,深入理解它们之间的区别,并在实际编程中正确使用,将有助于编写更加规范、健壮和易于维护的 JavaScript 代码。随着对 JavaScript 语言的不断学习和实践,能够更加熟练地运用这三种声明方式来满足不同的编程需求。————————————————原文链接:https://blog.csdn.net/2403_87566238/article/details/146290516
-
前言 在物联网开发中,通常一个服务端会和很多设备进行交互,设备普遍也会有文件升级的需求。如果下发了一个升级任务到很多个设备,这些设备在收到下载指令后同一个时间段执行升级任务,从服务器拉取升级文件,则会有将服务器带宽占满导致网络阻塞。所以我认为这里需要一个队列来分批次的执行这些任务。下面是我对下载任务实现代码的一些见解。1.工厂模式 关于工厂模式,我推荐大家可以看一下张老师的讲解,链接如下:Java设计模式之创建型:工厂模式详解(简单工厂+工厂方法+抽象工厂)_简单工厂模式,工厂方法模式,抽象工厂模式-CSDN博客2.数据库的设计 我是这样理解的,因为任务下发后不会直接发送给设备。所以需要将下载任务的数据进行存储。所以需要用到数据库将下载的任务和数据存起来,由队列不断地去执行该表中的任务。我这里用到的是MySQL数据库,表设计如下:CREATE TABLE `kwd_download_queue` ( `id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, `device_sn` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '设备编号', `type` int NULL DEFAULT NULL COMMENT '升级类型 具体的类型根据业务而定', `status` int NULL DEFAULT NULL COMMENT '任务状态 0 等待 1 执行 2 下载失败 3 下载成功', `create_time` datetime NULL DEFAULT NULL COMMENT '任务创建时间', `create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建人', `data_info` longblob NULL COMMENT '升级的数据信息', `execute_time` datetime NULL DEFAULT NULL COMMENT '执行时间', PRIMARY KEY (`id`) USING BTREE) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;3.代码实现 3.1 接口设计/** * 升级处理器接口,所有类型的升级处理器都需要实现该接口 */public interface UpgradeHandler { /** * 执行升级操作 * @param deviceSn 设备编号 * @param upgradeInfo 升级信息 * @return 操作结果,true表示成功,false表示失败 */ boolean executeUpgrade(String deviceSn, UpgradeInfo upgradeInfo); /** * 处理升级回调 * @param taskId 任务ID * @param success 是否成功 * @param message 回调消息 * @return 处理结果 */ boolean handleCallback(String taskId, boolean success, String message); /** * 获取处理器支持的升级类型 * @return 升级类型 */ int getUpgradeType();} /** * 升级信息内部类,用于存储在dataInfo字段中的JSON数据 */ @Data public static class UpgradeInfo { // 保留发送内容的原始JSON结构 private JsonNode sendContent; private String taskId; // 使用Map来存储动态的标识信息字段 @JsonIgnore private Map<String, Object> additionalProperties = new HashMap<>(); // 处理未知字段 @JsonAnySetter public void setAdditionalProperty(String name, Object value) { if (!"sendContent".equals(name)) { this.additionalProperties.put(name, value); } } @JsonAnyGetter public Map<String, Object> getAdditionalProperties() { return this.additionalProperties; } // 获取特定的标识信息 public Object getProperty(String key) { return additionalProperties.get(key); } // 获取sendContent作为字符串 public String getSendContentAsString() { try { return sendContent != null ? sendContent.toString() : null; } catch (Exception e) { return null; } } } 由于业务需求,在download_queue表中的dataInfo字段中,我添加了一些判断性的标识,是区分一些升级的类型的。sendContent中的内容是无需修改直接发送给设备的具体数据;additionalProperties是动态的附加属性,由于不同升级任务可能需要一些标识所以我偷懒使用了这个方法。 但是,这里设计的不太合理了,在此我建议大家扩充数据库字段进行标识,不要写在一个字段中,这样极其不易维护!!!我是纯因为懒才写到字段中用记忆的方式进行标识的哈哈哈。 好了,言归正传,因为不同的升级类型有不同的业务逻辑,需要不同的处理,所以需要先规范接口。让具体的升级类型的类去实现该接口。然后创建抽象类整合一些共有代码。3.2 抽象类设计import com.edison.device.entity.KwdDownloadQueue;import com.edison.device.mapper.KwdDownloadQueueMapper;import com.edison.device.service.impl.KwdDownloadQueueServiceImpl;import com.edison.device.upgrade.handler.UpgradeHandler;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired; /** * 升级处理器的抽象基类 * 实现通用逻辑,各具体处理器继承此类并实现特定逻辑 */@Slf4jpublic abstract class AbstractUpgradeHandler implements UpgradeHandler { @Autowired protected KwdDownloadQueueMapper downloadQueueMapper; @Override public boolean handleCallback(String taskId, boolean success, String message) { log.info("处理升级回调: taskId={}, success={}, message={}", taskId, success, message); // 查询任务 KwdDownloadQueue task = downloadQueueMapper.selectById(taskId); if (task == null) { log.error("任务不存在: {}", taskId); return false; } // 检查任务类型是否匹配 if (task.getType() != getUpgradeType()) { log.error("任务类型不匹配: expected={}, actual={}", getUpgradeType(), task.getType()); return false; } // 更新任务状态 int newStatus = success ? KwdDownloadQueue.DownloadStatus.COMPLETED.getCode() : KwdDownloadQueue.DownloadStatus.FAILED.getCode(); int updated = downloadQueueMapper.updateTaskStatus(taskId, newStatus); // 任务完成后的额外处理 if (updated > 0 && success) { onUpgradeSuccess(task); } else if (updated > 0 && !success) { onUpgradeFailed(task, message); } return updated > 0; } /** * 当升级成功时的后续处理 * 子类可以覆盖此方法实现特定逻辑 * * @param task 任务实体 */ protected void onUpgradeSuccess(KwdDownloadQueue task) { log.info("升级成功: taskId={}, deviceSn={}, type={}", task.getId(), task.getDeviceSn(), task.getType()); } /** * 当升级失败时的后续处理 * 子类可以覆盖此方法实现特定逻辑 * * @param task 任务实体 * @param errorMessage 错误信息 */ protected void onUpgradeFailed(KwdDownloadQueue task, String errorMessage) { log.warn("升级失败: taskId={}, deviceSn={}, type={}, error={}", task.getId(), task.getDeviceSn(), task.getType(), errorMessage); } /** * 记录升级日志 * 用于跟踪升级过程 * @param deviceSn 设备编号 * @param message 日志消息 */ protected void logUpgradeAction(String deviceSn, String message) { log.info("设备 {} 升级操作: {}", deviceSn, message); // 可以在此实现将日志写入数据库或其他存储 }} 之后使具体的不同升级类型的类去继承该抽象类实现代码,根据自己的业务去下发升级任务。3.3 具体任务类型的实现类@Component@Slf4jpublic class FileUpgradeHandler extends AbstractUpgradeHandler{ @Override public boolean executeUpgrade(String deviceSn, KwdDownloadQueueServiceImpl.UpgradeInfo upgradeInfo) { log.info("执行文件升级操作,设备:{},升级信息:{}", deviceSn, upgradeInfo); } @Override public int getUpgradeType() { return UpgradeTypes.FILE; // 具体的升级类型的标识,后续需要由工厂获取 } } 这是接口、抽象类和具体的实现类的基础代码,那么怎么使用呢?下面就要根据这些代码去创建一个工厂类。3.4 工厂类import com.edison.device.upgrade.handler.UpgradeHandler;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component; import javax.annotation.PostConstruct;import java.util.HashMap;import java.util.List;import java.util.Map; /** * 升级处理器工厂,负责根据升级类型获取对应的处理器 */@Slf4j@Componentpublic class UpgradeHandlerFactory { @Autowired private List<UpgradeHandler> upgradeHandlers; private final Map<Integer, UpgradeHandler> handlerMap = new HashMap<>(); /** * 初始化处理器映射 */ @PostConstruct public void init() { for (UpgradeHandler handler : upgradeHandlers) { handlerMap.put(handler.getUpgradeType(), handler); log.info("注册升级处理器:type={}, handler={}", handler.getUpgradeType(), handler.getClass().getSimpleName()); } } /** * 根据升级类型获取处理器 * @param upgradeType 升级类型 * @return 对应的处理器,如果不存在则返回null */ public UpgradeHandler getHandler(int upgradeType) { UpgradeHandler handler = handlerMap.get(upgradeType); if (handler == null) { log.error("未找到类型为{}的升级处理器", upgradeType); } return handler; }} 在项目启动时,所有继承AbstractUpgradeHandler的Bean对象都会被自动注入到upgradeHandlers属性中,然后通过init方法初始化处理器映射对象handlerMap,这样后续就可以通过方法getHandler根据不同的升级类型去调用不同的处理类进行任务的下发。3.5 队列工具类(队列实体服务层代码)import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;import com.edison.common.core.utils.bean.BeanUtils;import com.edison.device.domain.vo.DownloadQueueStats;import com.edison.device.domain.vo.DownloadQueueVO;import com.edison.device.entity.po.KwdDownloadQueue;import com.edison.device.mapper.KwdDownloadQueueMapper;import com.edison.device.service.KwdDownloadQueueService;import com.edison.device.upgrade.factory.UpgradeHandlerFactory;import com.edison.device.upgrade.handler.UpgradeHandler;import com.edison.device.utils.UpgradeUtils;import com.fasterxml.jackson.annotation.JsonAnyGetter;import com.fasterxml.jackson.annotation.JsonAnySetter;import com.fasterxml.jackson.annotation.JsonIgnore;import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.JsonNode;import com.fasterxml.jackson.databind.ObjectMapper;import lombok.Data;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional; import java.util.*;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.Executor;import java.util.concurrent.Executors; /** * (KwdDownloadQueue)表服务实现类 * */@Service@Slf4jpublic class KwdDownloadQueueServiceImpl extends ServiceImpl<KwdDownloadQueueMapper, KwdDownloadQueue> implements KwdDownloadQueueService { @Autowired private KwdDownloadQueueMapper downloadQueueMapper; @Autowired private UpgradeHandlerFactory upgradeHandlerFactory; @Autowired private ObjectMapper objectMapper; @Value("${download.concurrent.limit:20}") private int concurrentLimit; // 用于跟踪正在处理的任务,确保同一任务不被重复处理 private final ConcurrentHashMap<String, Boolean> processingTasks = new ConcurrentHashMap<>(); // 线程池用于异步执行下载任务 private final Executor downloadExecutor = Executors.newFixedThreadPool(10); /** * 创建新的下载任务 * 将VO转换为实体,设置初始状态为等待,并保存到数据库 * * @param downloadQueueVO 下载队列视图对象,包含设备编号、升级类型、创建人等信息 * @return 创建的任务ID */ @Override public String createDownloadTask(DownloadQueueVO downloadQueueVO) { KwdDownloadQueue downloadQueue = new KwdDownloadQueue(); BeanUtils.copyProperties(downloadQueueVO, downloadQueue); // 设置初始状态 downloadQueue.setStatus(KwdDownloadQueue.DownloadStatus.WAITING.getCode()); downloadQueue.setCreateTime(new Date()); // 序列化升级信息 try { if (downloadQueueVO.getUpgradeInfo() != null) { downloadQueue.setDataInfo(objectMapper.writeValueAsString(downloadQueueVO.getUpgradeInfo())); } } catch (JsonProcessingException e) { log.error("序列化升级信息时出错", e); throw new RuntimeException("创建下载任务时出错", e); } save(downloadQueue); log.info("已创建ID为的下载任务: {}", downloadQueue.getId()); return downloadQueue.getId(); } /** * 批量创建下载任务 * 适用于需要同时给多个设备下发升级指令的场景 * * @param downloadQueueVOList 下载队列视图对象列表 * @return 成功创建的任务数量 */ @Override @Transactional(rollbackFor = Exception.class) public int batchCreateDownloadTasks(List<DownloadQueueVO> downloadQueueVOList) { List<KwdDownloadQueue> downloadQueueList = new ArrayList<>(); for (DownloadQueueVO vo : downloadQueueVOList) { KwdDownloadQueue downloadQueue = new KwdDownloadQueue(); BeanUtils.copyProperties(vo, downloadQueue); // 设置初始状态和ID downloadQueue.setStatus(KwdDownloadQueue.DownloadStatus.WAITING.getCode()); downloadQueue.setCreateTime(new Date()); // 序列化升级信息 try { if (vo.getUpgradeInfo() != null) { downloadQueue.setDataInfo(objectMapper.writeValueAsString(vo.getUpgradeInfo())); } } catch (JsonProcessingException e) { log.error("序列化设备的升级信息时出错: {}", vo.getDeviceSn(), e); // 继续处理其他任务 continue; } downloadQueueList.add(downloadQueue); } // 批量保存 if (!downloadQueueList.isEmpty()) { saveBatch(downloadQueueList); log.info("批量创建 [{}] 个下载任务", downloadQueueList.size()); return downloadQueueList.size(); } return 0; } /** * 开始处理等待中的任务 * 根据配置的并发数,从等待队列中选取任务开始执行 * 确保同时执行的任务数不超过并发限制 * * @param batchSize 每次处理的批次大小,控制一次最多处理多少个任务 * @return 本次开始处理的任务数量 */ @Override// @Transactional(rollbackFor = Exception.class) //根据业务需求添加 public int processWaitingTasks(int batchSize) { // 获取当前正在执行的任务数 LambdaQueryWrapper<KwdDownloadQueue> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(KwdDownloadQueue::getStatus, KwdDownloadQueue.DownloadStatus.EXECUTING.getCode()); long executingCount = count(queryWrapper); // 计算可以新启动的任务数 long availableSlots = Math.max(0, concurrentLimit - executingCount); if (availableSlots <= 0) { log.debug("没有可用于新下载任务的插槽。当前执行: {}", executingCount); return 0; } // 获取等待中的任务 long tasksToProcess = Math.min(availableSlots, batchSize); List<KwdDownloadQueue> waitingTasks = downloadQueueMapper.findWaitingTasks(tasksToProcess); int processedCount = 0; for (KwdDownloadQueue task : waitingTasks) { // 检查同一设备是否已有正在执行的任务 int deviceExecutingCount = downloadQueueMapper.countExecutingTasksByDevice(task.getDeviceSn()); if (deviceExecutingCount > 0) { log.debug("设备 {} 已具有正在执行的任务,跳过", task.getDeviceSn()); continue; } // 更新任务状态为执行中 boolean updated = updateTaskStatus(task.getId(), KwdDownloadQueue.DownloadStatus.EXECUTING.getCode()); if (!updated) { log.warn("未能更新任务的任务状态: {}", task.getId()); continue; } // 异步处理任务 final String taskId = task.getId(); // 获取升级类型示例,这里并未使用 String type = null; if (task.getDataInfo() != null && !task.getDataInfo().isEmpty()) { try { KwdDownloadQueueServiceImpl.UpgradeInfo upgradeInfo = objectMapper.readValue(task.getDataInfo(), KwdDownloadQueueServiceImpl.UpgradeInfo.class); type = UpgradeUtils.getUpgradeType(upgradeInfo); } catch (JsonProcessingException e) { log.error("解析升级信息时出错: {}", task.getId(), e); } } String finalType = type; downloadExecutor.execute(() -> { try { processingTasks.put(taskId, true); boolean success = processTask(taskId); // 如果发送指令失败,才标记为失败 if (!success) { updateTaskStatus(taskId, KwdDownloadQueue.DownloadStatus.FAILED.getCode()); } } catch (Exception e) { log.error("处理任务时出错: {}", taskId, e); updateTaskStatus(taskId, KwdDownloadQueue.DownloadStatus.FAILED.getCode()); } finally { processingTasks.remove(taskId); } }); processedCount++; } log.info("已开始处理 {} 个等待任务", processedCount); return processedCount; } /** * 处理单个任务,执行实际的下载操作 * 根据任务类型调用不同的升级处理器执行具体升级逻辑 * * @param taskId 任务ID * @return 处理结果,true表示成功,false表示失败 */ @Override public boolean processTask(String taskId) { // 1. 获取任务详情 KwdDownloadQueue task = getById(taskId); if (task == null) { log.warn("未找到任务: {}", taskId); return false; } // 2. 验证任务状态 - 只处理"执行中"状态的任务 if (task.getStatus() != KwdDownloadQueue.DownloadStatus.EXECUTING.getCode()) { log.warn("任务 {} 未处于执行状态", taskId); return false; } log.info("正在处理下载任务 {},类型 {},设备 {}", taskId, task.getType(), task.getDeviceSn()); try { // 3. 解析升级信息 (JSON数据转对象) KwdDownloadQueueServiceImpl.UpgradeInfo upgradeInfo = null; if (task.getDataInfo() != null && !task.getDataInfo().isEmpty()) { upgradeInfo = objectMapper.readValue(task.getDataInfo(), KwdDownloadQueueServiceImpl.UpgradeInfo.class); } if (upgradeInfo == null) { log.error("任务的升级信息无效: {}", taskId); return false; } // 4. 获取对应类型的升级处理器 (策略模式) UpgradeHandler handler = upgradeHandlerFactory.getHandler(task.getType()); if (handler == null) { log.error("找不到任务升级类型 {} 的处理程序: {}", task.getType(), taskId); return false; } // 5. 执行具体的升级操作 upgradeInfo.setAdditionalProperty("taskId", taskId); // 设置任务ID upgradeInfo.setTaskId(taskId); boolean result = handler.executeUpgrade(task.getDeviceSn(), upgradeInfo); log.info("任务 {} 已处理,并返回结果: {}", taskId, result); return result; } catch (Exception e) { log.error("处理任务时出错: {}", taskId, e); return false; } } /** * 更新任务状态 * 用于手动更新任务状态或任务执行完成后更新状态 * * @param taskId 任务ID * @param status 新状态值,对应DownloadQueue.DownloadStatus枚举 * @return 更新是否成功 */ @Override public boolean updateTaskStatus(String taskId, int status) { int updated = downloadQueueMapper.updateTaskStatus(taskId, status); return updated > 0; } /** * 获取设备的任务历史 * 查询指定设备的所有下载任务记录,按时间倒序排列 * * @param deviceSn 设备编号 * @param limit 限制返回的记录数量 * @return 任务历史列表 */ @Override public List<KwdDownloadQueue> getDeviceTaskHistory(String deviceSn, int limit) { return downloadQueueMapper.findDeviceTaskHistory(deviceSn, limit); } /** * 检查并处理超时任务 * 将长时间处于执行状态但未完成的任务标记为失败 * * @param timeoutMinutes 超时时间(分钟),超过这个时间仍未完成的任务会被标记为失败 * @return 处理的超时任务数量 */ @Override @Transactional(rollbackFor = Exception.class) public int checkAndHandleTimeoutTasks(int timeoutMinutes) { List<KwdDownloadQueue> timeoutTasks = downloadQueueMapper.findTimeoutTasks(timeoutMinutes); int handled = 0; for (KwdDownloadQueue task : timeoutTasks) { // 避免处理正在被处理的任务 if (processingTasks.containsKey(task.getId())) { continue; } log.warn("找到超时任务:{},标记为失败", task.getId()); boolean updated = updateTaskStatus(task.getId(), KwdDownloadQueue.DownloadStatus.FAILED.getCode()); if (updated) { if (task.getType() == 0){ // 获取升级类型 String type = null; if (task.getDataInfo() != null && !task.getDataInfo().isEmpty()) { try { KwdDownloadQueueServiceImpl.UpgradeInfo upgradeInfo = objectMapper.readValue(task.getDataInfo(), KwdDownloadQueueServiceImpl.UpgradeInfo.class); type = UpgradeUtils.getUpgradeType(upgradeInfo); } catch (JsonProcessingException e) { log.error("解析升级信息时出错: {}", task.getId(), e); } } } handled++; } } log.info("已处理[{}]个超时任务", handled); return handled; } /** * 获取下载队列统计信息 * 统计不同状态的任务数量,用于监控和展示 * * @return 队列统计信息对象 */ @Override public DownloadQueueStats getQueueStats() { DownloadQueueStats stats = new DownloadQueueStats(); LambdaQueryWrapper<KwdDownloadQueue> waitingQuery = new LambdaQueryWrapper<>(); waitingQuery.eq(KwdDownloadQueue::getStatus, KwdDownloadQueue.DownloadStatus.WAITING.getCode()); stats.setWaitingTasks(count(waitingQuery)); LambdaQueryWrapper<KwdDownloadQueue> executingQuery = new LambdaQueryWrapper<>(); executingQuery.eq(KwdDownloadQueue::getStatus, KwdDownloadQueue.DownloadStatus.EXECUTING.getCode()); stats.setExecutingTasks(count(executingQuery)); LambdaQueryWrapper<KwdDownloadQueue> failedQuery = new LambdaQueryWrapper<>(); failedQuery.eq(KwdDownloadQueue::getStatus, KwdDownloadQueue.DownloadStatus.FAILED.getCode()); stats.setFailedTasks(count(failedQuery)); LambdaQueryWrapper<KwdDownloadQueue> completedQuery = new LambdaQueryWrapper<>(); completedQuery.eq(KwdDownloadQueue::getStatus, KwdDownloadQueue.DownloadStatus.COMPLETED.getCode()); stats.setCompletedTasks(count(completedQuery)); return stats; } /** * 重试失败的任务 * 将指定的失败任务重新设置为等待状态,等待重新执行 * * @param taskId 任务ID * @return 操作是否成功 */ @Override public boolean retryFailedTask(String taskId) { return false; } /** * 取消等待中的任务 * 将等待中的任务删除或标记为已取消 * * @param taskId 任务ID * @return 操作是否成功 */ @Override public boolean cancelWaitingTask(String taskId) { return false; } /** * 获取设备当前正在执行的任务数 * 用于判断设备是否有正在进行的升级任务 * * @param deviceSn 设备编号 * @return 执行中的任务数 */ @Override public int getDeviceExecutingTaskCount(String deviceSn) { return 0; } /** * 清理历史任务 * 删除或归档指定时间之前的已完成/失败任务 * * @param daysBefore 天数,删除多少天之前的历史任务 * @return 清理的记录数 */ @Override public int cleanHistoryTasks(int daysBefore) { return 0; } /** * 升级信息类,用于存储在dataInfo字段中的JSON数据 */ @Data public static class UpgradeInfo { // 保留发送内容的原始JSON结构 private JsonNode sendContent; private String taskId; // 使用Map来存储动态的标识信息字段 @JsonIgnore private Map<String, Object> additionalProperties = new HashMap<>(); // 处理未知字段 @JsonAnySetter public void setAdditionalProperty(String name, Object value) { if (!"sendContent".equals(name)) { this.additionalProperties.put(name, value); } } @JsonAnyGetter public Map<String, Object> getAdditionalProperties() { return this.additionalProperties; } // 获取特定的标识信息 public Object getProperty(String key) { return additionalProperties.get(key); } // 获取sendContent作为字符串 public String getSendContentAsString() { try { return sendContent != null ? sendContent.toString() : null; } catch (Exception e) { return null; } } }} 这一块代码会比较杂,因为不便展示,我还删除了一些相关的业务代码,每个方法上都有注释,解释了方法的作用,因为有些接口我并没有使用的需求,所以我没有实现该接口与的完整方法,可以根据自己的业务来修改该方法。3.6 相关实体类import lombok.Data; /** * 下载队列统计数据 * 用于统计不同状态任务的数量,提供给前端展示或监控系统使用 */@Datapublic class DownloadQueueStats { /** * 等待中的任务数 */ private long waitingTasks; /** * 执行中的任务数 */ private long executingTasks; /** * 失败的任务数 */ private long failedTasks; /** * 已完成的任务数 */ private long completedTasks; /** * 获取总任务数 * @return 所有状态任务的总和 */ public long getTotalTasks() { return waitingTasks + executingTasks + failedTasks + completedTasks; } /** * 获取活跃任务数(等待中+执行中) * @return 活跃任务数量 */ public long getActiveTasks() { return waitingTasks + executingTasks; } /** * 获取完成率 * @return 完成率百分比 */ public double getCompletionRate() { long total = getTotalTasks(); return total > 0 ? (double) completedTasks / total * 100 : 0; } /** * 获取失败率 * @return 失败率百分比 */ public double getFailureRate() { long total = getTotalTasks(); return total > 0 ? (double) failedTasks / total * 100 : 0; }} import lombok.Data;import javax.validation.constraints.NotBlank;import javax.validation.constraints.NotNull;import java.util.Map; /*** 任务创建**/@Datapublic class DownloadQueueVO { /** * 设备编号 */ @NotBlank(message = "设备编号不能为空") private String deviceSn; /** * 类型:0系统升级 1文件升级 2背景图升级 3启动词升级 */ @NotNull(message = "升级类型不能为空") private Integer type; /** * 创建人 */ private String createBy; /** * 升级信息,包含url、版本号、MD5等 */ @NotNull(message = "升级信息不能为空") private Map<String, Object> upgradeInfo;} import java.util.Date;import java.io.Serializable; import com.baomidou.mybatisplus.annotation.IdType;import com.baomidou.mybatisplus.annotation.TableId;import lombok.Data;import io.swagger.annotations.ApiModel;import io.swagger.annotations.ApiModelProperty;import com.fasterxml.jackson.annotation.JsonFormat;import lombok.Getter;import org.springframework.format.annotation.DateTimeFormat; /** * @author * @since */@Data@ApiModel("下载队列实体类")public class KwdDownloadQueue implements Serializable { @ApiModelProperty(value = "${column.comment}") @TableId(type = IdType.ASSIGN_ID) private String id; /** * 设备编号 */ @ApiModelProperty(value = "设备编号") private String deviceSn; /** * 类型 0系统升级 1文件升级.... */ @ApiModelProperty(value = "类型 0系统升级 1文件升级....") private Integer type; /** * 任务状态 0 等待 1 执行 2 升级失败 3 升级成功 */ @ApiModelProperty(value = "任务状态 0 等待 1执行 2 失败 3历史") private Integer status; /** * 任务创建时间 */ @ApiModelProperty(value = "任务创建时间") @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date createTime; /** * 创建人 */ @ApiModelProperty(value = "创建人") private String createBy; /** * 升级信息 */ @ApiModelProperty(value = "升级信息") private String dataInfo; /** * 执行时间 */ @ApiModelProperty(value = "执行时间") @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date executeTime; /** * 任务类型枚举 */ public enum DownloadType { SYSTEM_UPGRADE(0, "系统升级"), FILE_UPGRADE(1, "文件升级"), private final int code; private final String desc; DownloadType(int code, String desc) { this.code = code; this.desc = desc; } public int getCode() { return code; } public String getDesc() { return desc; } } /** * 任务状态枚举 */ @Getter public enum DownloadStatus { WAITING(0, "等待中"), EXECUTING(1, "执行中"), FAILED(2, "失败"), COMPLETED(3, "完成"); private final int code; private final String desc; DownloadStatus(int code, String desc) { this.code = code; this.desc = desc; } }}3.7 定时任务执行器import com.edison.device.service.KwdDownloadQueueService;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.scheduling.annotation.Scheduled;import org.springframework.stereotype.Component; import java.util.concurrent.atomic.AtomicBoolean; /** * 下载队列调度器,定期处理等待中的任务和检查超时任务 */@Slf4j@Componentpublic class DownloadQueueScheduler { @Autowired private KwdDownloadQueueService downloadQueueService; @Value("${download.batch.size:15}") private int batchSize; @Value("${download.timeout.minutes:30}") private int timeoutMinutes; // 防止任务重叠执行的标志 private final AtomicBoolean processingFlag = new AtomicBoolean(false); private final AtomicBoolean timeoutCheckFlag = new AtomicBoolean(false); /** * 定期处理等待中的任务 * 每30秒执行一次 */ @Scheduled(fixedDelayString = "${download.process.interval:30000}") public void processWaitingTasks() { // 如果已经有一个处理任务在执行,则跳过本次执行 if (!processingFlag.compareAndSet(false, true)) { log.debug("另一个处理任务正在执行,跳过本次调度"); return; } try { log.info("开始处理等待中的下载任务,批次大小:{}", batchSize); int processed = downloadQueueService.processWaitingTasks(batchSize); log.info("本次处理了{}个下载任务", processed); } catch (Exception e) { log.error("处理下载任务时发生异常", e); } finally { // 重置标志,允许下次执行 processingFlag.set(false); } } /** * 定期检查超时任务 * 每5分钟执行一次 */ @Scheduled(fixedDelayString = "${download.timeout.check.interval:300000}") public void checkTimeoutTasks() { // 如果已经有一个超时检查在执行,则跳过本次执行 if (!timeoutCheckFlag.compareAndSet(false, true)) { log.debug("另一个超时检查正在执行,跳过本次调度"); return; } try { log.info("开始检查超时任务,超时时间:{}分钟", timeoutMinutes); int handled = downloadQueueService.checkAndHandleTimeoutTasks(timeoutMinutes); log.info("处理了{}个超时任务", handled); } catch (Exception e) { log.error("检查超时任务时发生异常", e); } finally { // 重置标志,允许下次执行 timeoutCheckFlag.set(false); } }} 这里使用了定时任务,扫表获取待执行的任务再通过不同的任务执行器去执行任务;由于任务可能长时间没有反馈,需要有一个超时扫表任务修改任务状态来防止一些任务没有及时反馈造成队列阻塞。3.8 设备升级回调接口import com.edison.common.core.domain.Result;import com.edison.device.entity.po.KwdDownloadQueue;import com.edison.device.service.KwdDownloadQueueService;import com.edison.device.upgrade.factory.UpgradeHandlerFactory;import com.edison.device.upgrade.handler.UpgradeHandler;import io.swagger.annotations.Api;import io.swagger.annotations.ApiOperation;import lombok.Data;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController; @Slf4j@RestController@RequestMapping("/upgrade/callback")@Api(tags = "设备升级回调接口")public class UpgradeCallbackController { @Autowired private KwdDownloadQueueService downloadQueueService; @Autowired private UpgradeHandlerFactory upgradeHandlerFactory; /** * 接收设备升级结果回调 * @param callback 回调信息 * @return 处理结果 */ @PostMapping @ApiOperation("设备升级结果回调") public Result<Boolean> handleCallback(@RequestBody UpgradeCallback callback) { log.info("收到设备升级回调:taskId={}, deviceSn={}, success={}, message={}", callback.getTaskId(), callback.getDeviceSn(), callback.isSuccess(), callback.getMessage()); try { // 查询任务 KwdDownloadQueue task = downloadQueueService.getById(callback.getTaskId()); if (task == null) { log.error("任务不存在:{}", callback.getTaskId()); return Result.error("任务不存在"); } // 验证设备编号 if (!task.getDeviceSn().equals(callback.getDeviceSn())) { log.error("设备编号不匹配:expected={}, actual={}", task.getDeviceSn(), callback.getDeviceSn()); return Result.error("设备编号不匹配"); } // 获取对应的升级处理器 UpgradeHandler handler = upgradeHandlerFactory.getHandler(task.getType()); if (handler == null) { log.error("未找到对应的升级处理器:type={}", task.getType()); return Result.error("未找到对应的升级处理器"); } // 处理回调 boolean result = handler.handleCallback( callback.getTaskId(), callback.isSuccess(), callback.getMessage()); return result ? Result.OK("回调处理成功", true) : Result.error("回调处理失败"); } catch (Exception e) { log.error("处理升级回调时发生异常", e); return Result.error("处理回调异常:" + e.getMessage()); } } /** * 升级回调信息 */ @Data public static class UpgradeCallback { /** * 任务ID */ private String taskId; /** * 设备编号 */ private String deviceSn; /** * 是否成功 */ private boolean success; /** * 回调消息 */ private String message; }}AI写代码 这里就需要所对接的设备(安卓或者硬件)去向服务端汇报升级结果,然后去修改队列中的升级状态。如果有需要修改设备表的升级状态的需求,可以在此类中添加自己的业务逻辑。但是在此功能开发完毕进入生产环境后,我发现了一个问题,如果升级信息的数据量特别大的情况下,即使表中的记录数不多,下载队列的那张表会检索的特别特别慢!!!然后我寻思着修改数据库中的字段类型为longblob(原来是longtext类型),依然不起作用。然后我暂时没有做处理,目前我所能想到的解决方法只有分表进行处理,将data_info这个字段牵出去用两张表来维护此功能。如果各位老师有更好的办法欢迎留言!————————————————原文链接:https://blog.csdn.net/YyyGxxx/article/details/148327760
-
查找字母出现的次数这道题的思路在后面的题目过程中能用到,所以先把这题给写出来题目要求:给出一个字符串数组,要求输出结果为其中每个字符串及其出现次数。思路:我们可以把数组里的字符串按顺序放进map中,对于没被放进去过的字符串,放进去次数为1,之前被放进过去的字符串,那就在其上重新放入,并把次数重新加1.举个例子,输出的内容是:"this", "dog", "cat", "cat", "this", "dog"现在是把每个元素放进去,在没遇到一样数据之前的过程,如是上面所示,如果遇到了一样的数据, 这个操作看起来可能是把第二个cat放进去了,但是实际上是把cat重新输入了,然后把Key值输入为2了。因为map其中节点的样子如上图所示。代码部分如下import java.util.HashMap;import java.util.Map;import java.util.Set; public class Test { public static Map<String, Integer> countWords(String[] words){ Map<String, Integer> map = new HashMap<>(); for(String word : words){ if(map.get(word) == null){ map.put(word, 1); }else { int val = map.get(word); map.put(word, val+1); } } return map; } public static void main(String[] args) { String[] words = {"this", "dog", "cat", "cat", "this", "dog"}; Map<String, Integer> map = countWords(words); Set<Map.Entry<String, Integer>> entryset = map.entrySet(); for (Map.Entry<String, Integer> entry : entryset){ System.out.println("Key: " + entry + " Val: " + entry.getKey()); } }}只出现一次的数字题目链接:只出现一次的数字 - 力扣(LeetCode)题目描述:给一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。思路:这里的思路和上面的 查找字母出现的次数 有些像。依次把元素放到set中,如果set中没有该元素,就把该元素放进去,如果有,就把这个元素从set中删去。最后输出set中的元素以 {1,2,3,4,1,2,3} 为例,当第一次往里放,没有遇到重复的元素时,如下图按照数组的顺序,接着向下放,就会遇到重复的元素,这时候就要把set中的元素给删除了 后面的2,3也要依次从set中删除。public static int singleNumber(int[] nums){ HashSet<Integer> set = new HashSet<>(); for (int i = 0; i < nums.length; i++) { if(set.contains(nums[i])){ set.remove(nums[i]); }else{ set.add(nums[i]); } } for (int i = 0; i < nums.length; i++) { if(set.contains(nums[i])){ return nums[i]; } } return -1; } public static void main(String[] args) { int[] array = {1,2,3,4,1,2,3}; System.out.println(singleNumber(array)); }运行结果如下坏键盘打字题目链接:题目描述:旧键盘上坏了几个键,于是在敲一段文字的时候,对应的字符就不会出现。现在给出应该输入的一段文字、以及实际被输入的文字,请你列出肯定坏掉的那些键。输入在两行中分别给出应该输入的文字、以及实际输入的文字按照发现顺序,在一行中输出坏掉的键。其中英语字母只输出大写,每个坏键只输入一次。示例输入7_This_is_a_test_hs_s_a_es输出7TI题目思路:该题的思路在于如何找出坏键,这里提供一种思路,先把实际输入的数据放到set中,然后再把应该输入的文字遍历一遍,如果其中有set中没有的数据,那些没有的数据便是坏掉的键。public static void func(String str1, String str2){ //将字符串大写 str1 = str1.toUpperCase(); str2 = str2.toUpperCase(); HashSet<Character> setAct = new HashSet<>(); for (int i = 0; i < str2.length(); i++) { char ch = str2.charAt(i); setAct.add(ch); } for (int i = 0; i < str1.length(); i++) { char ch = str1.charAt(i); if(!setAct.contains(ch)){ System.out.print(ch); } } } public static void main(String[] args) { func("7_This_is_a_test", "_hs_s_a_es"); }这样的代码还是存在问题,没办法把其中重复出现的元素给消去,输出的结果是现在问题变成了如何去重,这部分不难能想到,我们可以创建一个setBroken来存放已经查找到的坏键,如果set和setBroken中都没有这个元素才打印.public class Test { public static void func(String str1, String str2){ str1 = str1.toUpperCase(Locale.ROOT); str2 = str2.toUpperCase(Locale.ROOT); HashSet<Character> setAct = new HashSet<>(); for (int i = 0; i < str2.length(); i++) { char ch = str2.charAt(i); setAct.add(ch); } //第一步是把不同的数给挑出来,然后对于重复输出的数据给去重 HashSet<Character> setBroken = new HashSet<>(); for (int i = 0; i < str1.length(); i++) { char ch = str1.charAt(i); if(!setAct.contains(ch) && !setBroken.contains(ch)){ setBroken.add(ch); System.out.print(ch); } } } public static void main(String[] args) { func("7_This_is_a_test", "_hs_s_a_es"); }}AI写代码输出结果为这次的内容就到这里,我们下篇文章再见————————————————原文链接:https://blog.csdn.net/xiaochuan_bsj/article/details/143368533
-
栈的概念(Stack)栈是常见的线性数据结构,栈的特点是以先进后出的形式,后进先出,先进后出,分为栈底和栈顶栈应用于内存的分配,表达式求值,存储临时的数据和方法的调用等。例如这把枪,第一发子弹是最后发射的,第一发子弹在栈底,而最新安装上去的子弹在栈的顶部,只有将上面的子弹打完(栈顶的数据走完),最后一发子弹才会射出栈的实现栈的实现是基于简单的数组形成的,我们可以将它想象成连续的数组,而栈的顺序是由后放到前放模拟实现栈的方法:push(放入一个元素到栈中)pop(提取栈顶的一个元素,并将在其栈中消除)peek(查看栈顶的元素)size(查看栈中的大小)empty(栈中是否为空)full(栈是否满了)代码import java.util.Arrays;public class MyStack implements IStack { private int[] elem; private int top;//数组的栈顶,以及数组栈中存放元素的数量 private static final int DEFAULT_CAPACITY = 10;//这里是初始容量 public MyStack() { elem = new int[DEFAULT_CAPACITY]; top = -1;//数组下标从0开始 } @Override public void push(int item) { if (full()) { //如果满了就扩容 elem = Arrays.copyOf(elem, 2 * elem.length); } elem[++top] = item; } @Override public int pop() throws RuntimeException { try { if (empty()) { throw new RuntimeException("栈为空"); } } catch (RuntimeException e) { e.printStackTrace(); } return elem[top--];//return返回后删除栈顶的元素 } @Override public int peek() { if (empty()) { throw new RuntimeException("栈为空"); } return elem[top];//返回栈顶元素 } @Override public int size() { return top+1;//去除数组0 } @Override public boolean empty() { return top == -1; } @Override public boolean full() { return top == elem.length;//count==当前的elem的总长度为true }}队列(Queue)队列是由先进先出的线性数据结构,采用的是先进先出,后进后出,如果要插入元素的话就是从入队尾巴方向插入,而删除作为出队要在头尾删除。队列的方法模拟实现队列(双链表实现)public class MyQueue implements IQueue{ static class Queue{ public int elem; public Queue next; public Queue prev; public Queue(int elem) { this.elem = elem; } } public Queue head; public Queue last; public int size=0; @Override public void offer(int val) { Queue queue = new Queue(val); if (this.head == null) { this.head = queue; this.last = queue; ++size; return; } this.last.next=queue; this.last.prev=this.head; this.last=last.next; ++size; } @Override public int poll() { if(this.head==null){ throw new RuntimeException("没有要丢掉的队列"); } Queue cur =this.head; if(this.head.next==null){ return -1; } this.head=this.head.next; this.head.prev=null; size--; return cur.elem; } @Override public int peek() { if(this.head!=null){ return this.head.elem; } return 0; } @Override public int size() { return size; }}循环队列(循环数组实现)数组实现队列的循环需要引入一个公式(目前的下标值+1)%当前数组的长度(index+1)%array.length,下标值从0开始少一个数,当index+1就是当前的总长度时,公式后的值一定为下标0。 private int[] array; private int front; private int rear; public MyCircularQueue(int k) { array=new int[k+1]; front=0;//初始位置 rear=0; } public boolean enQueue(int value) { //入列 if(isFull()){ //这里如果容量已经满了,需要先删除后在进行插入 return false; } array[rear]=value;//rear下标获取元素 rear=(rear+1)%array.length;//rear最终循环为0下标 return true; } public boolean deQueue() { //出列 if(isEmpty()){ //为空返回false return false; } front=(front+1)%array.length;//front只需要往后走 return true; } public int Front() { if(isEmpty()){ return -1; } return array[front]; } public int Rear() { if(isEmpty()){ return -1; } //这里三木操作符判断是否为0如果为0,将rear回退到最后一个位置,不为0则-1 int temp = (rear==0)?array.length-1:rear-1; return array[temp]; } public boolean isEmpty() { return front==rear; } public boolean isFull() { return (rear+1)%array.length==front; }}用队列实现栈因为队列是先进先出的,而我们的栈是先进后出的,两种线性结构的关系是颠倒的,一个队列是不能完成的,我们需要两个队列互相工作来完成辅助队列先获取数值,保证辅助队列是最后一个拿到值的,然后将主队列的值给到辅助队列,在交换两个队列的数值,因为队列关系先进先出,每一次最后一个值就是队列先出的数值主队列不为空,将主队列的元素都poll出放到辅助栈中,使用一个tmp来将主队列(这里主队列已经遍历完)和辅助队列交换 Queue<Integer> q1;//主队列 Queue<Integer> q2;//辅助队列 public MyStack() { q1=new LinkedList<>();//构造方法 q2=new LinkedList<>(); } public void push(int x) { q2.offer(x); while(!q1.isEmpty()){//主队列不为空,则将主队列出列给到辅助队列 q2.offer(q1.poll()); } //走到这里主队列是为空 Queue tmp=q1; q1=q2; q2=tmp; //将两个队列交换 } public int pop() { return q1.poll(); } public int top() { return q1.peek(); } public boolean empty() { return q1.isEmpty(); }}用栈来实现队列栈来实现队列,栈是先进后出的顺序,而队列是先进先出的顺序将push都放到a栈中当我们peek或者是要删除的时候,我们都将a栈的元素pop给b栈,这样b栈已经有了我们的元素但是我们还需要考虑的是丢掉元素后如果在一起添加元素到a栈呢,这里我们给一个条件,如果b的栈不为空时,我们仍然用b栈的队列如果a为空,这两个栈都是空的说明没有元素直接返回-1,如果a不为空的话且b没有新的元素b继续捕获新的a栈中所有的元素class MyQueue { Stack<Integer> A; Stack<Integer> B; public MyQueue() { A=new Stack<>(); B=new Stack<>(); } public void push(int x) { A.push(x); } public int pop() { int check=peek(); B.pop(); return check; } public int peek() { //先判断b是否是空的,如果不是空的直接返回,是空才可以往下走 if(!B.isEmpty())return B.peek(); //因为b还不是空的,所以不需要将a栈放到b中 if(A.isEmpty())return -1; while(!A.isEmpty()){ B.push(A.pop());//将所有的a放到b中 } return B.peek(); } public boolean empty() { return A.isEmpty()&&B.isEmpty(); //a和b都为空才为空 }}总结栈分为栈顶和栈底,最先进的为栈底,最后进的为栈顶。队列分为队头和队尾,最先进的为队头,最后进的为队尾。————————————————原文链接:https://blog.csdn.net/weixin_60489641/article/details/143723419
-
写在前面几个Java哥们儿瞪着满屏的报错,脸都快贴屏幕上了——项目deadline催命呢,这场景,熟吧?憋屈吧?可你扭头看看隔壁组,人家正端着咖啡杯,有说有笑地做测试呢!为啥?人家刚用了个叫飞算JavaAI的东西,把整个电商平台的后端代码,“唰”一下给整出来了!乖乖,这世道,真变了?说飞算JavaAI,你可别想岔了。它不是你写代码时蹦出来的那种“小补丁”,顶多算个“单词提示”。这玩意儿是动真格的——全球头一个专门伺候Java的,能直接给你“吐”出一整套、能跑、能用的项目代码! 背后是正经搞技术的飞算公司,牛人不少,钱也厚实。它牛在哪?简单说,就是把咱原来那套写代码的苦逼流程,给“掀桌子”了。你跟它叨咕一句“弄个订单管理系统”,它吭哧吭哧就给你整出接口、数据库、业务逻辑全套家伙事儿,直接能跑!科幻片?不,现在真有兄弟在用了。为啥说这玩意儿能救命?专治各种“工伤”!跟产品经理“鸡同鸭讲”?拜拜了您嘞! 产品老哥嘴里的“用户画像”,你以为是打标签?结果他要的是猜用户下一步买啥!来回掰扯,跟传话游戏似的,心累得慌!JavaAI咋整?你直接跟它唠嗑(说话都行),它就能整明白你要啥,连你没想到的(比如商品视频咋存咋管)都能给你拎出来。沟通成本?直接砍半!烦死人的CURD“搬砖”?丢给它! 建表?写增删改查接口?配那些乱七八糟的依赖包?这些破事儿占了大把时间,干完还没啥成就感,纯纯的“工具人”!JavaAI就猛了,点一下,Maven/Gradle项目骨架、标准代码、配置文件,全套齐活! 省下的功夫,琢磨点有意思的技术难点,不香吗?早点下班陪女朋友(如果有的话)不香吗?看见老代码就想跑?它能当“老中医”! 那些用老掉牙的Hibernate写的“祖传屎山”,看着就头大,重构?跟考古挖坟没区别,生怕动一下就塌了!JavaAI自带本地“老中医”功能,能帮你把这堆老古董“号号脉”,再看看现在有啥好用的新玩意儿,给你出个靠谱的升级方案,至少心里有底了。这玩意儿到底有啥能耐?Lethehong给你盘盘道兄弟们都说它是“六边形战士”,真不是瞎吹:嘴皮子一动,设计图就来了:你就说“搞个会员积分系统”,它立马给你列出要哪些接口、数据库表长啥样,连字段啥类型、主键咋设都给你整得门儿清。它肚子里专门琢磨过Java的“脾气”,设计出来的东西,扩展性好,不容易“牵一发动全身”。复杂业务不怕翻车?它有“防呆”招儿! 搞多张表一起操作、或者一堆人同时抢资源(高并发)?心里打鼓怕出幺蛾子吧?它能把复杂的业务逻辑掰开了、揉碎了,变成一步步能走的,还提前帮你瞅瞅哪儿可能打架。更神的是,你改了点小地方,它还能偷偷把相关的逻辑也调顺溜,有效防止“改一行代码,整个系统嗝屁”的惨案(这痛,扎心不?)。代码风格看不上眼?按你的规矩来! 嫌弃生成的代码太死板、没个性?简单!你直接跟它说你们组有啥“家规”(比如“DTO必须验数据”、“不准在代码里写死数字”),它生成的代码,立马就规规矩矩按你的“家规”来,跟你们组自己人码的一模一样。老系统不敢大动?它“小刀慢割”! 面对一堆陈年老代码,2.0版本多了个“一块一块生成”的功能,贼实用。你可以挑着某个接口或者功能,单独让它生成新代码,还能马上看到效果。往老系统里塞的时候,也不用提心吊胆怕把整个系统搞崩了。谁在用?反正不是摆设!刚入行的小白:被Spring Boot那些注解绕得七荤八素?用它生成个标准项目直接跑起来,边改边看边学,比干啃教程快多了,上手贼快!被deadline追着跑的苦命团队:真有兄弟(做医疗平台的)用了,仨小时,订单模块搞定! 搁以前,吭哧吭哧手写至少三天!省下的时间,人家转头就去搞更核心的算法优化了,效率杠杠的。总被“需求误解”气哭的产品经理:这回牛了,能直接甩给开发一个“能跑”的技术方案!再也不用背锅说“我明明说的是A,你们咋做出个C?”了,腰杆都直了!想少掉点头发的技术老大(CTO):用上它的规则引擎统一代码风格,Code Review的破事儿直接少了一大半! 团队代码看起来清清爽爽,老大也省心,少熬点夜,头发能多留几根。别小看它,可能真要“变天”别人还在吵吵AI写的代码片段靠不靠谱,飞算JavaAI已经玩得更深了:它把咱们这帮写Java的,从流水线上拧螺丝的“码农”,变成了指挥AI“施工队”干活的“包工头”(架构师)。有个用了的CTO老哥说的大实话:“以前兄弟们80%的劲儿都耗在写基础代码和擦屁股(修Bug)上了,现在?能腾出手来琢磨点真正有技术含量的、创新的东西了!”飞算这家公司,野心不小。之前搞的SoFlu软件机器人就吹过“一个人就能扛一个项目,十个人能当百人用”,在银行、医院这些地方都用上了。现在这个JavaAI是他们的“大招”,配上低代码、数据工具,搞了个给企业用的“数字化套餐” ——这架势,明摆着是要抢传统软件外包公司那万亿级别的大蛋糕啊!如何安装飞算 JavaAI步骤一:从 IDEA 插件市场安装点击导航-插件,打开应用市场,搜索相关关键字"CalEx JavaAl"或"飞算"都能搜索到,找到CalEx-JavaAl 后点击安装步骤二:注册飞算 JavaAl重启 Inteli IDEA 工具,请先注册账号并登录,登录成功后即可开始体验“智能分析”、"JavaChat”"智能问答“等功能。新项目开发”一键生成完工程文档,”“老项目二次开发”本地化智能分析深度理解+自定义开发规范。 步骤三:在日常开发中使用飞算JavaAlJava AI真的有那么神? 测试项目的优缺点 这里我正好有一个项目在线,于是我就让他给我一个文件中的优缺点 优点清晰的项目结构:文件结构符合标准的Maven项目结构,易于管理和扩展。明确的依赖管理:使用Maven管理依赖,版本明确,便于维护和更新。常用的库版本:使用的MySQL驱动和MyBatis版本都是比较稳定的版本,适合大2多数应用场景。缺点JUnit作用域设置不正确:JUnit的依赖作用域被设置为compile,而通常情况下应该设置为test,以避免将测试依赖打包到生产环境中。缺少日志库依赖:项目中没有包含日志库(如SLF4J或Log4j),这在实际应用中通常是必要的,以便于调试和监控。缺少其他实用依赖:例如数据库连接池(如HikariCP)、Spring框架(如果需要事务管理和依赖注入)等,这些在实际项目中可能会提高开发效率和应用性能。 Java AI制作Java算命这里生成代码的速度已经超越了绝大部分的AI编程工具我想利用Java写一个算命的程序,请你给我给出完整的代码 package com.test; import java.util.Scanner; public class FortuneTeller { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("请输入您的出生年份:"); int birthYear = scanner.nextInt(); scanner.close(); String fortune = tellFortune(birthYear); System.out.println("您的命运预测如下:"); System.out.println(fortune); } private static String tellFortune(int birthYear) { switch (birthYear % 12) { case 0: return "您属鼠,今年可能会遇到很多新的开始和机会。"; case 1: return "您属牛,今年可能会有很多稳定和收获。"; case 2: return "您属虎,今年可能会有挑战和冒险,但也充满力量。"; case 3: return "您属兔,今年可能会有快乐和轻松,但也需要注意健康。"; case 4: return "您属龙,今年可能会有领导力和创造力的展现。"; case 5: return "您属蛇,今年可能会有智慧和耐心的增长。"; case 6: return "您属马,今年可能会有活力和速度的提升。"; case 7: return "您属羊,今年可能会有和谐和温暖的感受。"; case 8: return "您属猴,今年可能会有灵活性和适应性的增强。"; case 9: return "您属鸡,今年可能会有勤奋和节俭的习惯。"; case 10: return "您属狗,今年可能会有忠诚和友谊的支持。"; case 11: return "您属猪,今年可能会有幸福和满足的感觉。"; default: return "无法预测的命运。"; } }}飞算JavaAI vs 国内其他AI编程工具跟通义灵码比较最大的区别就是专注度通义灵码支持Java、Python、C++等多种语言,但飞算JavaAI就专门死磕Java这一门语言。这就像一个是全科医生,一个是心脏科专家的区别。我之前同时用过这两个,感受挺明显的:通义灵码生成代码时,经常给你一些通用的模板,但涉及到Java特有的一些优化和最佳实践,就显得有点浅飞算JavaAI生成的代码遵循Java代码编写的最佳实践,代码风格统一、规范,可读性强代码生成的完整度差别很大这个差别真的很明显。通义灵码在生成复杂业务逻辑代码时,生成的代码结构有时不够清晰,需要开发者花费更多时间去梳理和优化。我拿同一个需求测试过,通义灵码给我生成了几个代码片段,我还得自己组装。飞算JavaAI直接给我一套完整的工程代码,连数据库建表语句都有了。跟文心快码比较百度这个确实厉害,但思路不一样文心快码支持超过100种主流编程语言,覆盖了从系统编程到Web开发、移动应用开发等多个领域。功能很全面,但问题还是老毛病——太泛了。我试过用文心快码做个电商系统,它能理解我的需求,也能给代码,但给的都是一些标准的CRUD操作。想要一些高级功能,比如分布式锁、缓存策略这些,就比较吃力。飞算JavaAI专注于Java单一语言开发,对Java语言特性和编程规范有深入理解,能生成高质量、符合行业最佳实践的Java代码。在处理复杂业务逻辑时,它真的能生成结构清晰、逻辑严谨的代码。跟豆包MarsCode比较字节的这个工具我用得不多主要原因是实测下来,感觉和GitHub Copilot和通义灵码都有差距,说实话是有点失望的。可能是因为发布时间比较晚,还在持续优化中。不过豆包MarsCode有个优势是它除了编程助手,还提供了云端开发环境。但纯粹从代码生成质量来说,跟飞算JavaAI比还是有明显差距的。实际使用建议如果你是:Java专业开发者:强烈推荐飞算JavaAI,真的能大幅提升效率多语言开发者:可以考虑通义灵码或文心快码个人学习者:通义灵码免费,可以先试试企业级项目:飞算JavaAI在代码质量和完整性上更有保障说实话,用过飞算JavaAI之后,再用其他工具总感觉缺点什么。就像习惯了自动挡汽车,再开手动挡总觉得麻烦。当然,这也可能是因为我主要做Java开发的原因。不过有一点要说明,飞算JavaAI目前主要专注后端,如果你要做前端开发,可能还是得配合其他工具使用。写在最后凌晨的办公室,咖啡机还在那儿“咕噜咕噜”响。但原来那密集的键盘“交响乐”少了,多了点飞算JavaAI干活时那种低沉的“嗡嗡”声。一个开发兄弟指着屏幕,乐了:“搞定!订单退款逻辑跑通了,嘿,连测试多人同时退款的代码都给我备好了!” 他那组人已经开始收拾包,张罗着去吃宵夜了。为啥这么潇洒?因为明天产品要的新需求讨论,他们今晚就能把演示版(Demo)整出来。当AI把那些重复的、费脑子的“搬砖”活儿扛了,咱们这帮写Java的脑子,总算能腾出来,干点更带劲、更有创造性的活儿了——比如,想想宵夜点啥烤串? (或者,早点回家睡觉?)————————————————原文链接:https://blog.csdn.net/2301_76341691/article/details/148697903
-
一、Java发展史 Java最初由Sun公司的“Green”项目组开发,用于智能家电设备,最初名为Oak。因商标问题,1995年更名为“Java”(灵感源于印尼爪哇岛的咖啡)。发行版本 发行时间 发行的各版本及其特征Java 1995年 Java语言诞生Java 1.0 1996年 首个正式版本,包含基础类库和Applet支持Java 1.1 1997年 引入内部类(Inner Class)、Java Beans、JDBC(数据库连接)和反射APIJava 1.2 1998年 JDK 1.2发布,更名为Java 2,分为三个平台:J2SE(标准版)、J2EE(企业版)、J2ME(微型版)Java 1.3 2000年 引入HotSpot JVM、JNDI(Java命名与目录接口)Java 1.4 2002年 新增正则表达式、断言(Assert)、NIO(非阻塞I/O)和日志APIJava 5.0 2004年 引入泛型、注解、枚举等革命性特性,为强调版本重要性,Sun将内部版本号1.5公开命名为5.0,此后版本号逐渐简化Java 6.0 2006年 Sun将产品线更名为Java SE/EE/ME,终结“J2”前缀,并宣布开源(OpenJDK)2009年 Oracle以74亿美元收购财务困境的Sun公司,Java正式归属OracleJava 7.0 2011年 Oracle首个大版本,支持菱形语法、多异常捕获,但因收购过渡期特性较少Java 8.0 2014年 继JDK 5后最大更新,引入Lambda表达式、Stream API、新日期时间库。LTS(长期支持)版本Java 9.0 2017年 发布周期改为每半年发布一次版本,每三年推出LTS(长期支持)版本Java 10.0 2018年 废弃“1.x”格式,直接使用主版本号(如JDK 10而非JDK 1.10)Java EE移交Eclipse基金会,重命名为Jakarta EE(如包名从javax.*改为jakarta.*)Java 11.0 2018年 新增HTTP客户端API、局部变量类型推断(var)并移除部分过时功能。LTS(长期支持)版本… … Java21.0 2023年 被视为继Java 8后的新一代主流版本,生态支持(如框架适配率)快速提升。LTS(长期支持)版本二、Java技术体系平台1、JavaSEJavaSE 的全称是 Java Platform Standard Edition(Java 平台标准版)面向桌面级应用(如Windows下的应用程序),提供完整的Java核心API,是其他平台(JavaEE、JavaME)的基础JavaSE和JDK的关系JavaSE(规范):定义接口、抽象类、具体类以及JVM的行为和约束(定义语言和API应该是什么样)例:JavaSE规范要求必须有一个ArrayList类,它实现List接口,支持动态扩容JDK(实现):提供这些接口和类的具体代码实现(按照规则实现并提供开发工具和运行环境)例1:OracleJDK的ArrayList源码中,具体实现了扩容机制(如默认扩容1.5倍)例2:OpenJDK的ArrayList可能实现相同的逻辑,但代码细节可能有细微差异(如注释、内部优化)历史名称:早期称为J2SE(JDK 6之前)2、JavaEEJavaEE 的全称是 Java Platform Enterprise Edition(Java 平台企业版)在Java SE基础上扩展了大量企业级API(如Servlet、JSP、EJB),提供分布式计算、事务管理、安全性等企业级功能JavaEE接口由官方规范定义,具体实现由应用服务器(Tomcat、WildFly)或第三方库(Hibernate、ActiveMQ)提供自JDK 10起由Oracle移交Eclipse基金会管理,更名为Jakarta EE历史名称:曾用名J2EE(JDK 6之前)3、JavaMEJavaME 的全称是 Java Platform Micro Edition(Java 平台微型版)针对移动终端(手机、PDA等)的轻量级平台,精简了Java SE的API并加入移动设备支持随着 Android 和 iOS 的普及,JavaME 的使用逐渐减少历史名称:曾用名J2ME4、三者关系JavaSE 是基础:JavaEE 和 JavaME 均基于 JavaSE 的核心功能构建JavaEE 是扩展:在 JavaSE 基础上增加企业级服务规范(如 Servlet、JPA、EJB)JavaME 是精简:仅保留 JavaSE 部分功能,并添加针对微型设备的特性三、Java程序运行机制及运行过程1、Java的跨平台性2、Java虚拟机(核心机制)JVM 是一个虚拟的计算机,具有指令集并使用不同的存储区域。负责执行指令,管理数据、内存、寄存器,包含在JDK 中对于不同的平台,有不同的虚拟机Java 虚拟机机制屏蔽了底层运行平台的差别,实现了“一次编译,到处运行”四、Java语言环境搭建1、JDK(Java开发工具包)定义:JDK是用于开发Java应用程序的完整工具包,包含编译、调试、文档生成等开发工具以及运行环境组成部分:JRE:JDK中内置了JRE(包含核心类库),确保开发时可以直接运行程序开发工具:如编译器javac(将Java源代码编译为字节码)、调试器jdb、文档工具javadoc等JDK特有的工具类库:如:tools.jar,支持编译器(javac)、调试器(jdb)等工具的运行(位于JDK的lib目录下)用途:开发者必须安装JDK,才能编写、编译和调试Java程序2、JRE(Java运行时环境)定义:JRE是运行已编译Java程序所需的最小环境,无需开发功能组成部分:JVM(Java虚拟机) :负责执行字节码,实现跨平台特性JRE中的核心类库:以java.*包的形式存在,例如rt.jar、resource.jar下java.lang、java.util等(位于JRE的lib目录下,并由BootstrapClassLoader自动加载)JRE中的扩展类库:以javax.*包的形式组织,例如javax.sql等(JRE的lib/ext目录下,由ExtensionClassLoader加载)用途:普通用户只需安装JRE即可运行Java程序(如.jar或.class文件),无需开发工具3、环境变量及作用3.1、JAVA_HOME该环境变量的值是Java的安装路径,一些Java版本的软件和工具需要用到该变量例如,当Windows平台上JDK的安装目录为“C:\java\jdk8”时,设置如下所示JAVA_HOME=C:\java\jdk813.2、CLASSPATH该环境变量用于指明Java字节码文件(.class文件)的位置默认情况下,如果未设置CLASSPATH,Java启动JVM后,会在当前目录下寻找字节码文件,一旦设置了CLASSPATH,JVM会在指定目录下查找字节码文件环境变量CLASSPATH的值一般为一个以分号“;”作为分隔符的路径列表,设置如下CLASSPATH=.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar;1“.”表示当前目录,因为设置CLASSPATH会覆盖JVM的默认操作(查找当前目录),所以这里需要加上“.”dt.jar是 Java 开发工具包(JDK)中用于为 IDE 提供 Swing/AWT 组件的设计时元数据(如属性、事件描述),支持通过拖拽和图形化界面进行可视化开发的核心类库文件tools.jar的作用:包含编译工具(如javac)所需的类库Java5之前,若用户未显式配置CLASSPATH环境变量JVM不会在当前目录查询.class文件,所以需要配置CLASSPATH但从Java 5(2004年发布)开始,默认情况,无需显式配置CLASSPATH,JVM会自动搜索当前目录和核心类库3.3、PATH该环境变量指定一个路径列表,用于搜索可执行文件执行一个可执行文件时,如果该文件不能在当前路径下找到,则依次寻找PATH中的每一个路径,直至找到。例如:PATH=.;%JAVA_HOME%\bin;1这样可以在命令行中直接使用java和javac命令,而不需要指定完整路径,否则就会出现以下错误:不建议在PATH环境变量中添加当前目录"."的主要原因:如果当前目录"."被加入PATH,当用户进入公共可写目录/tmp时,攻击者可能在该目录下放置与系统命令同名的恶意程序例如:黑客在/tmp目录下创建名为ls的木马文件,当用户(尤其是root用户)执行ls命令时,会优先执行当前目录下的恶意程序而非系统标准的/bin/ls,导致权限泄露或数据被破坏————————————————原文链接:https://blog.csdn.net/qq_35512802/article/details/148105022
-
最近在复习JavaScript的基础知识,和第一次学确实有了很不一样的感受,第一次学的比较浅,但是回头再进行学习的时候,发现有很多遗漏的东西,所以今天想分享一下新学到的知识,后面会一点一点补充更新JavaScript的数据结构有8个,分别是number string boolean object undefined null 还有es6新增的symbol和bigint,今天主要分享一下null undefined number,其他的等复习完会及时更新的null:null是一个独立的数据类型,表示一个空值或者是一个对象没有值null有几个特殊的用法,操作如下:1.当使用Number方法来识别null的时候,输出为0 console.log(Number(null)) //02.对null实现一些运算符操作(可以将null当做0来进行计算) console.log(2 + null) //2 console.log(2 * null) //0undefined:undefined比较特殊,表示未定义,比如你在生命一个变量,但是没给他赋值的时候,然后检测该变量的类型,输出就是undefined let a console.log(a) //undefined 1.当使用Number方法来识别undefined的时候,输出为NaNconsole.log(Number(undefined)) //NaN2.对undefined实现一些运算符操作(可以将undefined当做没有值来进行计算) console.log(undefined + 2) //NaN console.log(undefined * 2) //NaN 3.当使用undefined和null来进行比较的时候,非严格模式下,两者是相等的console.log(null == undefined) //trueconsole.log(null === undefined) //falsenumber:number是用来表示整数和浮点数已经NaN的数据类型,JavaScript的底层没有整数1.所有的数字都是使用64位浮点数来进行存储console.log(1 === 1.0) //true2.当小数在进行相加的时候,具有误差console.log((0.3 - 0.2) === 0.1) //false3.当一个计算的数大于2的53次方,计算就不准确了console.log(Math.pow(2, 53) === Math.pow(2, 53) + 1) //true4.当一个数大于2的1024次方,就会溢出,如果小于2的-1075次方,会溢出为0console.log(Math.pow(2, 1024)) //Infinityconsole.log(Math.pow(2, -1075) ) //05.+0和-0在很多情况下+0和-0是一样的,但是只有当他们表示分母的时候,会有不一样的结果console.log(+0 === -0) //trueconsole.log(1 / +0 === 1 / -0) //false6.NaN表示number类型,当对NaN进行幂运算的时候,输出为1,其他情况下都为NaNconsole.log(NaN ** 0) //17.进制十进制表示没有前导0的数值,二进制前缀(0b/0B),八进制前缀(0o/0O),十六进制前缀(0x/0X),特殊情况: 有前导0的数值会被视为八进制,但是如果前导0后面有数字8和9,则该数值被视为十进制。console.log(099) //99console.log(088) //88console.log(077) //63 8.infinity运算1. 范围:Infinity大于一切数值(除了NaN),-Infinity小于一切数值(除了NaN)。console.log(Infinity > -100) //trueconsole.log(-Infinity < -100) //falseconsole.log(Infinity > NaN) //falseconsole.log(-Infinity < NaN) //false2. Infinity与undefined计算,返回的都是NaN。console.log(Infinity + undefined) //NaNconsole.log(Infinity - undefined) //NaNconsole.log(Infinity * undefined) //NaNconsole.log(Infinity / undefined) //NaN3. Infinity减去或除以Infinity,得到NaN。console.log(Infinity - Infinity) //NaNconsole.log(Infinity / Infinity) //NaN4.0乘以Infinity,返回NaN;0除以Infinity,返回0;Infinity除以0,返回Infinity。console.log(0 * Infinity) //NaNconsole.log(0 / Infinity) //0console.log(Infinity / 0) //Infinity5.Infinity与null计算时,null会转成0,等同于与0的计算。只用相乘的时候,返回NaNconsole.log(Infinity + null) //Infinityconsole.log(Infinity - null) //Infinityconsole.log(Infinity * null) //NaNconsole.log(Infinity / null) ————————————————原文链接:https://blog.csdn.net/2301_81253185/article/details/148673464
-
1、流程引擎介绍Flowable 是一个使用 Java 编写的轻量级业务流程引擎。Flowable 流程引擎可用于部署 BPMN2.0 流程定义(用于定义流程的行业 XML 标准),创建这些流程定义的流程实例,进行查询,访问运行中或历史的流程实例与相关数据,等等。Java 领域另一个流程引擎是 Activiti,不这两个东西只要会使用其中一个,另一个就不在话下。咱就不废话了,上代码吧。 2、创建项目首先创建一个 Spring Boot 项目,引入 Web、和 MySQL 驱动两个依赖,如下图:项目创建成功之后,引入 flowable 依赖,如下:<dependency> <groupId>org.flowable</groupId> <artifactId>flowable-spring-boot-starter</artifactId> <version>6.7.2</version></dependency>这个会做一些自动化配置,默认情况下,所以位于 resources/processes 的流程都会被自动部署。接下来在 application.yaml 中配置一下数据库连接信息,当项目启动的时候会自动初始化数据库,将来流程引擎运行时候的数据会被自动持久化到数据库中。spring: datasource: username: root password: 123 url: jdbc:mysql:///flowable?serverTimezone=Asia/Shanghai&useSSL=false配置完成后,就可以启动项目了。项目启动成功之后,flowable 数据库中就会自动创建如下这些表,将来流程引擎相关的数据都会自动保存到这些表中。默认的表比较多,截图只是其中一部分。 3、画流程图画流程图算是比较有挑战的一个步骤了,也是流程引擎使用的关键。官方提供了一些流程引擎绘制工具,感兴趣的小伙伴可以自行去体验;IDEA 也自带了一个流程可视化的工具,但是特别难用。这里说一下常用的 IDEA 插件 Flowable BPMN visualizer,如下图:装好插件之后,在 resources 目录下新建 processes 目录,这个目录下的流程文件将来会被自动部署。接下来在 processes 目录下,新建一个 BPMN 文件(插件装好了就有这个选项了),如下:来画个请假的流程,就叫做 ask_for_leave.bpmn20.xml,注意最后面的 .bpmn20.xml 是固定后缀。文件创建出来之后,右键单击,选择 View BPMN(Flowable) Diagram,就打开了可视化页面了,就可以来绘制自己的流程图了。请假流程画出来是这样:员工发起一个请假流程,首先是组长审核,组长审核通过了,就进入到经理审核,经理审核通过了,这个流程就结束了,如果组长审核未通过或者经理审核未通过,则流程给员工发送一个请假失败的通知,流程结束。来看下这个流程对应的 XML 文件,一些流程细节会在 XML 文件中体现出来,如下:<process id="ask_for_leave" name="ask_for_leave" isExecutable="true"> <userTask id="leaveTask" name="请假" flowable:assignee="#{leaveTask}"/> <userTask id="zuzhangTask" name="组长审核" flowable:assignee="#{zuzhangTask}"/> <userTask id="managerTask" name="经理审核" flowable:assignee="#{managerTask}"/> <exclusiveGateway id="managerJudgeTask"/> <exclusiveGateway id="zuzhangJudeTask"/> <endEvent id="endLeave" name="结束"/> <startEvent id="startLeave" name="开始"/> <sequenceFlow id="flowStart" sourceRef="startLeave" targetRef="leaveTask"/> <sequenceFlow id="modeFlow" sourceRef="leaveTask" targetRef="zuzhangTask"/> <sequenceFlow id="zuzhang_go" sourceRef="zuzhangJudeTask" targetRef="managerTask" name="通过"> <conditionExpression xsi:type="tFormalExpression"><![CDATA[${checkResult=='通过'}]]></conditionExpression> </sequenceFlow> <sequenceFlow id="zuzhang_reject" sourceRef="zuzhangJudeTask" targetRef="sendMail" name="拒绝"> <conditionExpression xsi:type="tFormalExpression"><![CDATA[${checkResult=='拒绝'}]]></conditionExpression> </sequenceFlow> <sequenceFlow id="jugdeFlow" sourceRef="managerTask" targetRef="managerJudgeTask"/> <sequenceFlow id="flowEnd" name="通过" sourceRef="managerJudgeTask" targetRef="endLeave"> <conditionExpression xsi:type="tFormalExpression"><![CDATA[${checkResult=='通过'}]]></conditionExpression> </sequenceFlow> <sequenceFlow id="rejectFlow" name="拒绝" sourceRef="managerJudgeTask" targetRef="sendMail"> <conditionExpression xsi:type="tFormalExpression"><![CDATA[${checkResult=='拒绝'}]]></conditionExpression> </sequenceFlow> <serviceTask id="sendMail" flowable:exclusive="true" name="发送失败提示" isForCompensation="true" flowable:class="org.javaboy.flowable.AskForLeaveFail"/> <sequenceFlow id="endFlow" sourceRef="sendMail" targetRef="askForLeaveFail"/> <endEvent id="askForLeaveFail" name="请假失败"/> <sequenceFlow id="zuzhangTask_zuzhangJudeTask" sourceRef="zuzhangTask" targetRef="zuzhangJudeTask"/></process>结合 XML 文件来解释一下这里涉及到的 Flowable 中的组件:<process> :表示一个完整的工作流程。<startEvent> :工作流中起点位置,也就是图中的绿色按钮。<endEvent> :工作流中结束位置,也就是图中的红色按钮。<userTask> :代表一个任务审核节点(组长、经理等角色),这个节点上有一个 flowable:assignee 属性,这表示这个节点该由谁来处理,将来在 Java 代码中调用的时候,需要指定对应的处理人的 ID 或者其他唯一标记。<serviceTask>:这是服务任务,在具体的实现中,这个任务可以做任何事情。<exclusiveGateway> :逻辑判断节点,相当于流程图中的菱形框。<sequenceFlow> :链接各个节点的线条,sourceRef 属性表示线的起始节点,targetRef 属性表示线指向的节点,图中的线条都属于这种。流程图这块松哥和大家稍微说一下,咋一看这个图挺复杂很难画,但是实际上只要认认真真去捋一捋这里边的各个属性,基本上很快就明白到底是怎么一回事。 4、开发接口接下来写几个接口,来体验一把流程引擎。在正式体验之前,先来熟悉几个类,这几个类一会写代码会用到。 4.1 Java 类梳理ProcessDefinition这个最好理解,就是流程的定义,也就相当于规范,每个 ProcessDefinition 都会有一个 id。 ProcessInstance这个就是流程的一个实例。简单来说,ProcessDefinition 相当于是类,而 ProcessInstance 则相当于是根据类 new 出来的对象。 ActivityActivity 是流程标准规范 BPMN2.0 里面的规范,流程中的每一个步骤都是一个 Activity。 ExecutionExecution 的含义是流程的执行线路,通过 Execution 可以获得当前 ProcessInstance 当前执行到哪个 Activity了。 TaskTask 就是当前要做的工作。实际上这里涉及到的东西比较多,不过这里先整一个简单的例子,所以上面这些知识点暂时够用了。 4.2 查看流程图在正式开始之前,先准备一个接口,用来查看流程图的实时执行情况,这样方便查看流程到底执行到哪一步了。具体的代码如下:@RestControllerpublic class HelloController { @Autowired RuntimeService runtimeService; @Autowired TaskService taskService; @Autowired RepositoryService repositoryService; @Autowired ProcessEngine processEngine; @GetMapping("/pic") public void showPic(HttpServletResponse resp, String processId) throws Exception { ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(processId).singleResult(); if (pi == null) { return; } List<Execution> executions = runtimeService .createExecutionQuery() .processInstanceId(processId) .list(); List<String> activityIds = new ArrayList<>(); List<String> flows = new ArrayList<>(); for (Execution exe : executions) { List<String> ids = runtimeService.getActiveActivityIds(exe.getId()); activityIds.addAll(ids); } /** * 生成流程图 */ BpmnModel bpmnModel = repositoryService.getBpmnModel(pi.getProcessDefinitionId()); ProcessEngineConfiguration engconf = processEngine.getProcessEngineConfiguration(); ProcessDiagramGenerator diagramGenerator = engconf.getProcessDiagramGenerator(); InputStream in = diagramGenerator.generateDiagram(bpmnModel, "png", activityIds, flows, engconf.getActivityFontName(), engconf.getLabelFontName(), engconf.getAnnotationFontName(), engconf.getClassLoader(), 1.0, false); OutputStream out = null; byte[] buf = new byte[1024]; int legth = 0; try { out = resp.getOutputStream(); while ((legth = in.read(buf)) != -1) { out.write(buf, 0, legth); } } finally { if (in != null) { in.close(); } if (out != null) { out.close(); } } }}这就一个工具,没啥好说的,一会大家看完后面的代码,再回过头来看这个接口,很多地方就都懂了。 4.3 开启一个流程为了方便,接下来的代码都在单元测试中完成。首先来开启一个流程,代码如下:String staffId = "1000";/** * 开启一个流程 */@Testvoid askForLeave() { HashMap<String, Object> map = new HashMap<>(); map.put("leaveTask", staffId); ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("ask_for_leave", map); runtimeService.setVariable(processInstance.getId(), "name", "javaboy"); runtimeService.setVariable(processInstance.getId(), "reason", "休息一下"); runtimeService.setVariable(processInstance.getId(), "days", 10); logger.info("创建请假流程 processId:{}", processInstance.getId());}首先由员工发起一个请假流程,map 中存放的 leaveTask 是在 XML 流程文件中提前定义好的,提前定义好当前这个任务创建之后,该由谁来处理,这里是假设由工号为 1000 的员工来发起这样一个请假流程。同时,还设置了一些额外信息。ask_for_leave 是在 XML 文件中定义的一个 process 的名称。好啦,现在执行这个单元测试方法,执行完成后,控制台会打印出当前这个流程的 id,拿着这个 id 去访问 4.2 小节的接口,结果如下:可以看到,请假用红色的框框起来了,说明当前流程走到了这一步。 4.4 将请求提交给组长接下来,就需要将这个请假流程向后推进一步,将请假事务提交给组长,代码如下:String zuzhangId = "90";/** * 提交给组长审批 */@Testvoid submitToZuzhang() { //员工查找到自己的任务,然后提交给组长审批 List<Task> list = taskService.createTaskQuery().taskAssignee(staffId).orderByTaskId().desc().list(); for (Task task : list) { logger.info("任务 ID:{};任务处理人:{};任务是否挂起:{}", task.getId(), task.getAssignee(), task.isSuspended()); Map<String, Object> map = new HashMap<>(); //提交给组长的时候,需要指定组长的 id map.put("zuzhangTask", zuzhangId); taskService.complete(task.getId(), map); }}首先利用 staffId 查找到当前员工的 id,进而找到当前员工需要执行的任务,遍历这个任务,调用 taskService.complete 方法将任务提交给组长,注意在 map 中指定组长的 id。提交完成后,再去看流程图片,如下:可以看到,流程图走到组长审批了。 4.5 组长审批组长现在有两种选择,同意或者拒绝,同意的代码如下:/** * 组长审批-批准 */@Testvoid zuZhangApprove() { List<Task> list = taskService.createTaskQuery().taskAssignee(zuzhangId).orderByTaskId().desc().list(); for (Task task : list) { logger.info("组长 {} 在审批 {} 任务", task.getAssignee(), task.getId()); Map<String, Object> map = new HashMap<>(); //组长审批的时候,如果是同意,需要指定经理的 id map.put("managerTask", managerId); map.put("checkResult", "通过"); taskService.complete(task.getId(), map); }}通过组长的 id 查询组长的任务,同意的话,需要指定经理,也就是这个流程下一步该由谁来处理。拒绝的代码如下:/** * 组长审批-拒绝 */@Testvoid zuZhangReject() { List<Task> list = taskService.createTaskQuery().taskAssignee(zuzhangId).orderByTaskId().desc().list(); for (Task task : list) { logger.info("组长 {} 在审批 {} 任务", task.getAssignee(), task.getId()); Map<String, Object> map = new HashMap<>(); //组长审批的时候,如果是拒绝,就不需要指定经理的 id map.put("checkResult", "拒绝"); taskService.complete(task.getId(), map); }}拒绝的话,就没那么多事了,直接设置 checkResult 为拒绝即可。假设这里执行了同意,那么流程图如下: 4.6 经理审批经理审批和组长审批差不多,只不过经理这里是最后一步了,不需要再指定下一位处理人了,同意的代码如下:/** * 经理审批自己的任务-批准 */@Testvoid managerApprove() { List<Task> list = taskService.createTaskQuery().taskAssignee(managerId).orderByTaskId().desc().list(); for (Task task : list) { logger.info("经理 {} 在审批 {} 任务", task.getAssignee(), task.getId()); Map<String, Object> map = new HashMap<>(); map.put("checkResult", "通过"); taskService.complete(task.getId(), map); }}拒绝代码如下:/** * 经理审批自己的任务-拒绝 */@Testvoid managerReject() { List<Task> list = taskService.createTaskQuery().taskAssignee(managerId).orderByTaskId().desc().list(); for (Task task : list) { logger.info("经理 {} 在审批 {} 任务", task.getAssignee(), task.getId()); Map<String, Object> map = new HashMap<>(); map.put("checkResult", "拒绝"); taskService.complete(task.getId(), map); }}4.7 拒绝流程如果组长拒绝了或者经理拒绝了,也有相应的处理方案,首先在 XML 流程文件定义时,如下:<serviceTask id="sendMail" flowable:exclusive="true" name="发送失败提示" isForCompensation="true" flowable:class="org.javaboy.flowable.AskForLeaveFail"/>如果请假被拒绝,会进入到这个 serviceTask,serviceTask 对应的处理类是 org.javaboy.flowable.AskForLeaveFail,该类的代码如下:public class AskForLeaveFail implements JavaDelegate { @Override public void execute(DelegateExecution execution) { System.out.println("请假失败。。。"); }}也就是请假失败会进入到这个方法中,现在就可以在这个方法中该干嘛干嘛了。————————————————原文链接:https://blog.csdn.net/2402_87298751/article/details/148666102
-
1. File类的使用这里主要介绍对文件增删改查的操作 , 不是对文件中的内容进行增删改查1.1 构造方法方法 说明File(File parent, String child) 根据⽗⽬录 + 孩⼦⽂件路径,创建⼀个新的 File 实例File(String pathname) 根据⽂件路径创建⼀个新的 File 实例,路径可以是绝对路径或者相对路径File(String parent, String child) 根据⽗⽬录 + 孩⼦⽂件路径,创建⼀个新的 File 实例,⽗⽬录⽤路径表⽰1.2 常用方法方法 说明getParent() 返回 File 对象的⽗⽬录⽂件路径getName() 返回 FIle 对象的纯⽂件名称getPath() 返回 File 对象的⽂件路径getAbsolutePath() 返回 File 对象的绝对路径getCanonicalPath() 返回 File 对象的修饰过的绝对路径exists() 判断 File 对象描述的⽂件是否真实存在isDirectory() 判断 File 对象代表的⽂件是否是⼀个⽬录isFile() 判断 File 对象代表的⽂件是否是⼀个普通⽂件createNewFile() 根据 File 对象,⾃动创建⼀个空⽂件。成功创建后返回 truedelete() 根据 File 对象,删除该⽂件。成功删除后返回 truedeleteOnExit() 根据 File 对象,标注⽂件将被删除,删除动作会到 JVM 运⾏结束时才会进⾏list() 返回 File 对象代表的⽬录下的所有⽂件名listFiles() 返回 File 对象代表的⽬录下的所有⽂件,以 File 对象表⽰mkdir() 创建 File 对象代表的⽬录mkdirs() 创建 File 对象代表的⽬录,如果必要,会创建中间⽬录renameTo(File dest) 进⾏⽂件改名,也可以视为我们平时的剪切、粘贴操作canRead() 判断⽤⼾是否对⽂件有可读权限canWrite() 判断⽤⼾是否对⽂件有可写权限2. I/O流I/O是Input/Output的缩写。I/O技术是非常实用的技术,用于处理数据传输。如读/写文件,网络通讯等。Java程序中,对于数据的输入/输出操作以"流(stream)" 的方式进行。java.io包下提供了各种"流"类和接口,用以获取不同种类的数据,并通过方法输入或输出数据2.1 I/O流的分类按操作数据单位不同分为:字节流(8 bit),字符流(16 bit)按数据流的流向不同分为:输入流,输出流按流的角色的不同分为:节点流,处理流抽象基类 字节流 字符流输入流 InputStream Reader输出流 OutputStream Writer2.2 I/O体系Java的io流共涉及40多个类,实际上非常规则,都是以上述4个抽象基类派生的3. 字节流3.1 InputStream类InputStream 是抽象类 , 我们现在只关心从文件中读取,所以使用 FileInputStream类实例化对象FileInputStream类的构造方法方法 说明FileInputStream(File file) 通过指定的File对象来创建输入流。FileInputStream(String pathname) 通过指定文件的路径字符串来创建输入流。FileInputStream类的常见方法方法 说明int read() 从输入流中读取一个字节的数据。int read(byte[] b) 从输入流中读取一定数量的字节到字节数组中,返回长度。int read(byte[] b, int off, int len) 最多读取 len - off 字节的数据到 b中,放在从 off 开始,返回实际读到的数量;-1 代表以及读完了void close() 关闭字节流3.2 OutputStream类OutputStream 同样只是一个抽象类,要使用还需要具体的实现类。我们现在还是只关心写入文件中,所以使用 FileOutputStream类实例化对象FileOutputStream的构造方法方法 说明FileOutputStream(File file) 通过指定的File对象来创建输出流FilenOutputStream(String name) 通过指定文件的路径字符串来创建输出流FileOutputStream的常用方法方法 说明write(int b) 写入一个字节到文件。write(byte[] b) 将一个字节数组写入文件write(byte[] b, int off, int len) 从字节数组的指定位置开始,写入指定长度的字节到文件close() 关闭输出流,释放相关资源。4. 字符流4.1 Reader类FileReader类主要用于从文件中读取字符数据。它是一个字符输入流,继承自InputStreamReader(转换流),抽象基类为Reader。可以通过构造方法传入文件路径来创建FileReader的构造方法方法 说明FileReader(File file) 创建一个与指定文件对象相关联的FileReader。FileReader(String fileName) 创建一个与指定文件路径名相关联的FileReaderFileReader的常用方法方法 说明int read() 从输入流中读取一个字符,返回该字符的整数表示(到达文件末尾返回 -1)int read(char[] cbuf) 将字符读入数组。返回读取的长度void close() 关闭该流并释放与之关联的所有资源。4.2 Writer类FileWriter类用于将字符数据写入文件。FileWriter的常用方法方法 说明write(int c) 写入单个字符到文件中。write(char[] cbuf) 将字符数组写入文件中。write(String str) 写入字符串到文件中。write(String str, int off, int len) 写入字符串的一部分到文件中。flush() 刷新缓冲区,将数据写入文件。close() 关闭文件并释放相关资源。————————————————原文链接:https://blog.csdn.net/2401_82690001/article/details/148137889
-
找到setting.json文件通过Trae中的搜索功能,可以找到Trae所使用的配置文件。修改JDK版本前提:已经按照对应版本的JDK在setting.json文件中找到“java.import.gradle.java.home”和“metals.javaHome”两个配置项,改成你想要的JDK版本的路径即可。小技巧-快捷查找JDK路径可以在setting.json文件中搜索“terminal.integrated.profiles.osx”这个配置项,这个配置项中就是trae扫描到的本机中安装的JDK版本及路径,复制想要的JDK版本的路径,直接使用即可。————————————————原文链接:https://blog.csdn.net/u011924665/article/details/146208263
-
五、对象的构造及初始化5.1 如何初始化对象通过前面知识点的学习我们知道,在Java方法内部定义一个局部变量时,必须要初始化,否则会编译失败。public static void main(String[] args) {int a;System.out.println(a);}// Error:(26, 28) java: 可能尚未初始化变量a要让上述代码通过编译,非常简单,只需在正式使用变量之前给它设置初始值即可。public static void main(String[] args) { Date d = new Date(); d.printDate(); d.setDate(2021,6,9); d.printDate();}// 代码可以正常通过编译如果是对象,就需要调用之前写的 setDate 方法将具体的日期设置到对象中。通过上述例子我们发现两个问题:问题1:每次对象创建好后调用 setDate 方法设置具体日期显得比较麻烦,那么对象该如何初始化?问题2:局部变量必须初始化才能使用,而字段声明之后没有给值依然可以使用,这是因为字段具有默认初始值。为了解决问题1,Java引入了 构造方法,使得对象在创建时就能完成初始化操作。5.2 构造方法构造方法(也称为构造器)是一种特殊的成员方法,其主要作用是初始化对象。5.2.1 构造方法的概念public class Date { public int year; public int month; public int day; // 构造方法: // 名字与类名相同,没有返回值类型,设置为void也不行 // 一般情况下使用public修饰 // 在创建对象时由编译器自动调用,并且在对象的生命周期内只调用一次 public Date(int year, int month, int day){ this.year = year; this.month = month; this.day = day; System.out.println("Date(int,int,int)方法被调用了"); } public void printDate(){ System.out.println(year + "-" + month + "-" + day); } public static void main(String[] args) { // 此处创建了一个Date类型的对象,并没有显式调用构造方法 Date d = new Date(2021,6,9); // 输出Date(int,int,int)方法被调用了 d.printDate(); // 2021-6-9 }}构造方法的特点是:名字必须与类名相同,且没有返回值类型(连void都不行)。在创建对象时由编译器自动调用,并且在对象的生命周期内只调用一次。注意:构造方法的作用是对对象中的成员进行初始化,并不负责给对象开辟内存空间。5.2.2 构造方法的特性构造方法具有如下特性:名字必须与类名完全相同没有返回值类型,即使设置为void也不行创建对象时由编译器自动调用,且在对象生命周期内只调用一次(就像人的出生,每个人只能出生一次)支持重载:同一个类中可以定义多个构造方法,只要参数列表不同即可示例代码1:带参构造方法public class Date { public int year; public int month; public int day; // 构造方法:名字与类名相同,没有返回值类型,使用public修饰 // 在创建对象时由编译器自动调用,并且在对象的生命周期内只调用一次 public Date(int year, int month, int day) { this.year = year; this.month = month; this.day = day; System.out.println("Date(int, int, int)方法被调用了"); } public void printDate() { System.out.println(year + "-" + month + "-" + day); } public static void main(String[] args) { // 此处创建了一个Date类型的对象,并没有显式调用构造方法 Date d = new Date(2021, 6, 9); // 输出:Date(int, int, int)方法被调用了 d.printDate(); // 输出:2021-6-9 }}示例代码2:无参构造方法public class Date { public int year; public int month; public int day; // 无参构造方法:给成员变量设置默认初始值 public Date() { this.year = 1900; this.month = 1; this.day = 1; }}上述两个构造方法名字相同但参数列表不同,构成了方法的重载。如果用户没有显式定义构造方法,编译器会生成一个默认的无参构造方法。注意:一旦用户显式定义了构造方法,编译器就不会再生成默认构造方法了示例代码3:仅定义带参构造方法时默认构造方法不会生成// 带有三个参数的构造方法public Date(int year, int month, int day) { this.year = year; this.month = month; this.day = day;}public void printDate(){ System.out.println(year + "-" + month + "-" + day);}public static void main(String[] args) { Date d = new Date(); // 编译期报错,因为没有无参构造方法 d.printDate();}示例代码4:只有无参构造方法的情况public class Date { public int year; public int month; public int day; public void printDate(){ System.out.println(year + "-" + month + "-" + day); } public static void main(String[] args) { Date d = new Date(); d.printDate(); }}示例代码5:只有带参构造方法的情况public class Date { public int year; public int month; public int day; public Date(int year, int month, int day) { this.year = year; this.month = month; this.day = day; }}构造方法中可以通过 this(…) 调用其他构造方法来简化代码。注意:this(…) 必须是构造方法中的第一条语句,否则编译器会报错。示例代码6:正确使用this(…)实现构造器链public class Date { public int year; public int month; public int day; // 无参构造方法 -- 内部调用带参构造方法实现初始化 // 注意:this(1900, 1, 1);必须是构造方法中的第一条语句 public Date(){ // System.out.println(year); // 若取消注释则编译会失败 this(1900, 1, 1); // 以下赋值代码被省略,因为已在带参构造方法中完成初始化 // this.year = 1900; // this.month = 1; // this.day = 1; } // 带有三个参数的构造方法 public Date(int year, int month, int day) { this.year = year; this.month = month; this.day = day; }}注意:构造方法中的 this(…) 调用不能形成循环,否则会导致编译错误。public Date(){this(1900,1,1);}public Date(int year, int month, int day) {this();}/*无参构造器调用三个参数的构造器,而三个参数构造器有调用无参的构造器,形成构造器的递归调用编译报错:Error:(19, 12) java: 递归构造器调用*/在大多数情况下,我们使用 public 来修饰构造方法,但在特殊场景下(如实现单例模式)可能会使用 private 修饰构造方法。5.3 默认初始化在上文中提到的第二个问题:为什么局部变量在使用前必须初始化,而成员变量可以不初始化?要搞清楚这个过程,就需要知道 new 关键字背后所发生的一些事情:Date d = new Date(2021,6,9);在程序员看来只是一句简单的语句,但 JVM 层面需要做好多事情。下面简单介绍下:检测对象对应的类是否被加载,如果没有则加载为对象分配内存空间并先默认初始化处理并执行类中的 init 方法初始化分配好的空间 (说明:多个线程同时申请资源,JVM 要保证分配给对象的空间内干净。)即:对象空间被申请好之后,对象中包含的成员已经设置好了初始值,比如:Java中的 成员变量 会由 JVM 自动赋予默认值(如int类型为0,boolean类型为false等),而局部变量则需要显式初始化才能使用。Java为不同的数据类型提供了默认值,具体如下所示:数据类型 默认值byte 0char ‘\u0000’short 0int 0long 0Lboolean falsefloat 0.0fdouble 0.0dreference null这些默认值确保了即使对象的成员变量没有显式初始化,也不会发生错误,成员变量会有一个初始的稳定状态。设置对象头信息(关于对象内存模型后面会介绍)调用构造方法,给对象中各个成员赋值5.4 就地初始化就地初始化是指在成员变量声明时直接为它们赋初值。这种方法在代码的简洁性上具有优势,可以避免每次创建对象时重复设置成员变量的值。代码示例:public class Date { public int year = 1900; // 就地初始化 public int month = 1; // 就地初始化 public int day = 1; // 就地初始化 public Date() { // 构造方法在此处不需要再次初始化year、month、day,它们已经有默认值 } public void printDate() { System.out.println(year + "-" + month + "-" + day); } public static void main(String[] args) { Date d = new Date(); d.printDate(); // 输出:1900-1-1 }}在这个例子中,year、month 和 day 的值在声明时就被初始化为1900、1和1,确保了每次创建对象时这些值已经存在。六、封装6.1 封装的概念封装是面向对象编程的三大特性之一,其核心思想是将数据和操作数据的方法结合在一起,并对外隐藏实现细节。例如,电脑作为一个复杂的设备,用户只需通过开关、键盘和鼠标等接口进行交互,而不必关心内部CPU、显卡等工作原理。简单来说就是套壳屏蔽细节。6.2 访问限定符访问修饰符说明public:可以理解为一个人的外部接口,能被外部访问。protected:主要是给继承使用,子类继承后就能访问到。default(不写修饰符时即为默认访问修饰符):对于同一包内的类可见,不同包则不可见。private:只能在当前类中访问。(这部分要介绍完继承后才能完全理解)【说明】protected 主要是给继承使用;default 只能给同一个包内使用;按照自己的理解去记忆。示例代码:public class Computer { private String cpu; private String brand; private String memory; private String screen; public Computer(String brand, String cpu, String memory, String screen) { this.brand = brand; this.cpu = cpu; this.memory = memory; this.screen = screen; } public void boot() { System.out.println("开机"); } public void shutDown() { System.out.println("关机"); }}public class TestComputer { public static void main(String[] args) { Computer c = new Computer("华为", "i9", "16G", "4K"); c.boot(); c.shutDown(); }}注意:一般情况下,成员变量通常设置为 private,成员方法设置为 public。6.3 封装扩展之包6.3.1 包的概念包(Package)用于对类进行分组管理,有助于解决类名冲突并提高代码的组织性。例如,将相似功能的类归为同一包。为了更好的管理类,把多个类收集在一起成为一组,称为软件包6.3.3导入包在Java中,如果我们需要使用不在默认 java.lang 包中的类或接口,就需要使用 import 关键字来导入。例如,我们想使用 java.util.Date 这个类,就可以这样做:import java.util.Date;public class TestImport { public static void main(String[] args) { Date d = new Date(); System.out.println(d); }}这样就可以正常创建 Date 对象并使用。6.3.3全类名当不同包中存在同名的类时(如 java.util.Date 与 java.sql.Date 都叫 Date),可能会引发冲突。这时,可以使用 全类名(Fully Qualified Name)来指定使用哪个类:public class TestFullName { public static void main(String[] args) { // 使用全类名来区分两个Date类 java.util.Date d1 = new java.util.Date(); java.sql.Date d2 = new java.sql.Date(System.currentTimeMillis()); System.out.println(d1); System.out.println(d2); }}通过在创建对象时加上包名,就可以区分来自 java.util 和 java.sql 的 Date 类。6.3.4 静态导入从 Java 5 开始,支持使用 静态导入(import static)的方式将某个类中的 静态成员(常量或方法) 导入到当前类中,从而在调用时可以省略类名。示例代码:import static java.lang.Math.PI;import static java.lang.Math.random;public class TestStaticImport { public static void main(String[] args) { System.out.println(PI); // 直接使用PI常量 System.out.println(random()); // 直接使用random()方法 }}如果不使用静态导入,则需要写成:System.out.println(Math.PI);System.out.println(Math.random());6.3.5 IDE工具中的包结构当我们使用 IntelliJ IDEA 或 Eclipse 等 IDE 工具时,会在项目结构中直观地看到包名与文件夹一一对应。包名一般使用 小写 的域名反写形式(如 com.example.project),在 IDE 中会对应层级文件夹结构。在同一个包下,可以放置多个类文件,便于组织与管理。在实际开发中,合理划分包结构能让项目更易于维护和理解。6.3.6 包的访问权限控制举例Computer类位于com.bit.demo1包中,TestComputer位于com.bit.demo2包中:package com.bit.demo1;public class Computer { private String cpu; // cpu private String memory; // 内存 public String screen; // 屏幕 String brand; // 品牌 public Computer(String brand, String cpu, String memory, String screen) { this.brand = brand; this.cpu = cpu; this.memory = memory; this.screen = screen; } public void PowerOff(){ System.out.println("关机~~~"); } public void SurfInternet(){ System.out.println("上网~~~"); }}package com.bit.demo2;import com.bit.demo1.Computer;public class TestComputer { public static void main(String[] args) { Computer p = new Computer("HW", "i7", "8G", "13*14"); System.out.println(p.screen); // 公有属性,可以被其他包访问 // System.out.println(p.cpu); // 私有属性,不能被其他包访问 // System.out.println(p.brand); // brand是default,不允许被其他包中的类访问 }}注意:如果去掉前面的Computer类中的public修饰符,代码也会编译失败。6.3.7 常见的包java.lang:系统常用基础类(String,Object),此包从JDK1.1后自动导入。java.lang.reflect:Java反射机制包;java.net:进行网络编程开发包;java.sql:进行数据库开发的包;javax.util:Java提供的工具程序包(集合类等)非常重要;javalio.io:编程程序包。注意事项: import 和 C++ 的 #include 差别很大. C++ 必须 #include 来引入其他文件内容, 但是 Java 不需要.import 只是为了写代码的时候更方便.区别如下:Java 的 import:仅在编译时为你提供类或接口的简写路径,使你可以直接使用类名而不用写出完整的包名。实际上,编译器会在编译过程中通过类路径(classpath)去查找相应的类文件,而不会把代码插入到当前文件中。C/C++ 的 #include:预处理器会在编译之前将头文件的内容直接拷贝进源代码文件中,这种方式实际上是将文件的内容“粘贴”到包含它的文件里。因此,Java 的 import 只是一种简化引用的机制,并不涉及代码的复制。七、总结与展望在本篇文章中,我们围绕对象的构造与初始化、封装、包的管理以及访问权限等内容展开了详细讲解,帮助大家深入理解Java面向对象编程的核心概念。7.1 总结对象的构造与初始化构造方法:通过构造方法,我们可以在创建对象时立即为其成员赋值,保证对象在使用前处于有效状态。构造器重载与this调用:支持多个构造方法以及构造器链,让对象初始化更加灵活和简洁。默认初始化与就地初始化:成员变量会自动获得默认值,同时也可以在声明时直接赋值,避免重复代码。封装核心思想:将数据与操作数据的方法绑定在一起,通过访问限定符(public、private、protected、default)隐藏内部实现细节。访问控制:合理使用访问修饰符保护数据安全,提供公开接口供外部使用,增强了程序的健壮性和安全性。包的管理与导入包的概念:通过将相关类归为一组,实现代码的模块化管理和命名空间隔离,避免类名冲突。import语句:在编译时提供类名简写路径,与C/C++的#include机制不同,import不进行代码复制,仅起到标识和简化引用的作用。7.2 总结深入static成员在后续的内容中,我们将更详细地探讨static关键字的使用,包括静态变量、静态方法及其在类中的意义,帮助大家更好地理解类级别的共享特性。代码块与初始化块将介绍类中的初始化块和静态代码块,讨论它们在对象创建过程中的执行顺序和作用,为进一步掌握对象生命周期奠定基础。内部类与匿名类内部类是一种特殊的类定义方式,它能够更紧密地绑定外部类的成员,将在未来篇章中深入剖析其用法和设计思想。面向对象的设计原则随着对类和对象理解的深入,我们也将探讨更多面向对象设计的原则和模式,帮助大家构建更健壮、可维护的Java应用程序。———————————————— 原文链接:https://blog.csdn.net/2301_79849925/article/details/146015106
上滑加载中
推荐直播
-
华为云码道-AI时代应用开发利器2026/03/18 周三 19:00-20:00
童得力,华为云开发者生态运营总监/姚圣伟,华为云HCDE开发者专家
本次直播由华为专家带你实战应用开发,看华为云码道(CodeArts)代码智能体如何在AI时代让你的创意应用快速落地。更有华为云HCDE开发者专家带你用码道玩转JiuwenClaw,让小艺成为你的AI助理。
回顾中 -
Skill 构建 × 智能创作:基于华为云码道的 AI 内容生产提效方案2026/03/25 周三 19:00-20:00
余伟,华为云软件研发工程师/万邵业(万少),华为云HCDE开发者专家
本次直播带来两大实战:华为云码道 Skill-Creator 手把手搭建专属知识库 Skill;如何用码道提效 OpenClaw 小说文本,打造从大纲到成稿的 AI 原创小说全链路。技术干货 + OPC创作思路,一次讲透!
回顾中 -
华为云一键云上部署Openclaw 实现“龙虾自由”2026/04/11 周六 14:00-16:00
秦拳德-中软国际教育卓越研究院研究员
还在为搭建AI环境而焦头烂额、彻夜难眠? 还在苦苦等待复杂工具的缓慢响应、迟迟无法推进项目? 别再犹豫,快来华为云,一键部署OpenClaw,轻松告别 繁琐配置,即刻畅享极速体验!更有龙虾实操演示全程护 航,真正解放双手,让办公效率实现质的飞跃。限时重磅 福利火热来袭,干万Tokens等你来瓜分,机会难得,不容 错过!
即将直播
热门标签