-
CodeArts IDE For Java 什么时候才能更新下一个版本呢?现在已经停留在2.3.0很久了,现在如果只运行一个工程的话问题不大,只不过单元测试那里有点问题。但是我们公司是那种一个总的模块,然后下面一堆子模块来一起开发的,然后这些模块的最上级都是同一个父工程的。但是因为每个人的负责的模块不同,像我是负责几个模块开发的。但是我们是没有最上级模块的代码权限的。如果我要用这个IDE来进行开发的话,那么我要开很多个窗口,然后同时如果我是有引用另外模块的api的话,那么我要将那个模块用maven进行打包,然后我当前模块也要重新install才能用到那个模块api里面的内容。这样的话会大大的降低开发的效率。这种情况有没有办法解决的?我试过将这些模块都拉到同一个窗口里面,但是这样的话他就无法正确的识别我的工程了。
-
25年的第二次合集来了,涵盖多方面内容,供大家阅读。1.解读为什么@Autowired在属性上被警告,在setter方法上不被警告问题https://bbs.huaweicloud.com/forum/thread-0226176114797408069-1-1.html2.C 与 C++ 中的 const 常量与数组大小的关系对比分析【转】https://bbs.huaweicloud.com/forum/thread-0225176114407160080-1-1.html3. Java对象和JSON字符串之间的转换方法(全网最清晰)【转】https://bbs.huaweicloud.com/forum/thread-0218176114284451074-1-1.html4.Python中利用json库进行JSON数据处理详解【转】https://bbs.huaweicloud.com/forum/thread-0225176114218197079-1-1.html5.Python使用sys.path查看当前的模块搜索路径【转】https://bbs.huaweicloud.com/forum/thread-0225176047116377070-1-1.html6.PyTorch环境中CUDA版本冲突问题排查与解决方案【转】https://bbs.huaweicloud.com/forum/thread-0225176047065418069-1-1.html7.python 3.8 的anaconda下载方法【转】https://bbs.huaweicloud.com/forum/thread-02127176045871929063-1-1.html8.Python实现批量Excel拆分功能【转】https://bbs.huaweicloud.com/forum/thread-0218176045720424067-1-1.html9.Python中常用的四种取整方式分享【转】https://bbs.huaweicloud.com/forum/thread-0218176037491495065-1-1.html10.Python自动化处理手机验证码【转】https://bbs.huaweicloud.com/forum/thread-0210176037394123079-1-1.html11. Docker应用部署(Mysql、tomcat、Redis、redis)https://bbs.huaweicloud.com/forum/thread-0251176000289103057-1-1.html12.使用Python实现文件重命名的三种方法【转】https://bbs.huaweicloud.com/forum/thread-0210175967963879073-1-1.html13.python安装whl包并解决依赖关系的实现【转】https://bbs.huaweicloud.com/forum/thread-0226175967884244062-1-1.html14.Python轻松实现批量邮件自动化详解【转】https://bbs.huaweicloud.com/forum/thread-0251175967723301055-1-1.html15.Python脚本实现图片文件批量命名【转】https://bbs.huaweicloud.com/forum/thread-0226175967493359061-1-1.html16.Python中多线程和多进程的基本用法详解【转】https://bbs.huaweicloud.com/forum/thread-0251175967381425054-1-1.html17.鸿蒙NEXT开发案例:随机数生成https://bbs.huaweicloud.com/forum/thread-0226175856046738051-1-1.html18.鸿蒙NEXT开发案例:简体繁体转换器https://bbs.huaweicloud.com/forum/thread-0251175855906014051-1-1.html19.鸿蒙NEXT开发案例:血型遗传计算https://bbs.huaweicloud.com/forum/thread-0220175855724260049-1-1.html20.鸿蒙NEXT开发案例:数字转中文大小写https://bbs.huaweicloud.com/forum/thread-0225175850374977055-1-1.html
-
在 Spring 开发中,@Autowired 注解常用于实现依赖注入。它可以应用于类的 属性、构造器 或 setter 方法 上。然而,当 @Autowired 注解在 属性 上使用时,IntelliJ IDEA 等 IDE 会给出 Field injection is not recommended 的警告,而在 setter 方法 上使用 @Autowired 时却不会出现这个警告。1. 为什么 @Autowired 在属性上被警告?1.1 隐式依赖注入当 @Autowired 注解应用于类的 属性 上时,Spring 会直接注入该属性,而不通过构造函数或 setter 方法显式地传递依赖项。这种注入方式称为 字段注入(Field Injection)。字段注入 的缺点主要体现在以下几个方面:1.隐式依赖:通过字段注入,类的依赖关系是隐式的,无法在类的构造器或方法中显式地看到这些依赖。相对而言,构造器注入 和 setter 注入 可以使依赖关系更加明确。由于字段依赖是隐式注入的,开发者很难在不查看容器配置的情况下,快速了解一个类的所有依赖项。2.难以进行单元测试:字段注入的属性是隐式注入的,无法通过构造函数或 setter 方法显式传递。在单元测试中,手动注入模拟(mock)对象时,需要通过反射或者测试框架自动注入,这增加了测试的复杂性。与此相比,构造器注入和 setter 注入会使依赖关系显式可见,能够更方便地进行 单元测试。3.违反依赖倒置原则(DIP):在 依赖倒置原则 中,依赖关系应该通过 接口 或 抽象 进行注入,而不应该在类内部直接依赖于具体的实现。字段注入使得类的依赖更加隐式,可能会增加代码的耦合性。1.2 IDE 的警告:Field injection is not recommendedIntelliJ IDEA 等 IDE 会根据这些设计缺点发出警告,提示 @Autowired 注解不推荐使用在属性上。字段注入的方式可能会导致代码的可维护性差,容易出现一些潜在问题(如不清晰的依赖关系和难以测试的代码)。2. 为什么 @Autowired 在 setter 方法上不被警告?当 @Autowired 用于 setter 方法 时,Spring 会通过 setter 注入 方式将依赖项注入到对象的属性中。与字段注入不同,setter 注入方式具有以下优势:2.1 显式依赖注入显式依赖关系:使用 setter 方法注入,开发者可以明确看到类所依赖的组件。通过查看类的 setter 方法,其他开发者可以轻松理解该类的依赖关系。12345678public class MyService { private MyRepository repository; @Autowired public void setRepository(MyRepository repository) { this.repository = repository; }}符合依赖注入的设计原则:通过构造函数或 setter 方法注入依赖项,可以使类的依赖关系更加清晰,符合面向对象设计中的 依赖注入 和 单一职责原则。2.2 可选的依赖注入setter 注入适用于一些 可选依赖 的场景。如果某个依赖是可选的,可以通过 setter 方法来灵活注入,而不需要在构造器中强制要求依赖项的传入。1234@Autowiredpublic void setOptionalDependency(Optional<Dependency> dependency) { this.dependency = dependency.orElse(null);}2.3 易于测试由于 setter 方法可以手动设置对象的依赖,因此它可以使单元测试变得更简单。你可以通过 setter 方法为对象注入模拟(mock)依赖项,而不需要通过反射等复杂手段。12MyService myService = new MyService();myService.setRepository(mockRepository);3. 构造器注入 vs 字段注入 vs Setter 注入3.1 构造器注入(推荐)构造器注入 是 最推荐的依赖注入方式,它具有以下优势:强制依赖关系:通过构造器传递依赖项,可以确保所有的依赖项在对象创建时就已经被正确地注入。不可变性:构造器注入使得依赖项在对象创建时就被初始化,避免了运行时更改依赖项。易于测试:构造器注入使得所有的依赖项在构造时就显式提供,便于进行单元测试。12345678public class MyService { private final MyRepository repository; @Autowired public MyService(MyRepository repository) { this.repository = repository; }}3.2 Setter 注入(次推荐)Setter 注入 是一个灵活的选择,适用于依赖关系较为可选或后期可更改的场景。它具有以下特点:灵活性:可以在对象创建后修改依赖项。适用于可选依赖:如果某些依赖项是可选的,setter 注入能够方便地管理。12345678public class MyService { private MyRepository repository; @Autowired public void setRepository(MyRepository repository) { this.repository = repository; }}3.3 字段注入(不推荐)字段注入 是最简单的注入方式,但并不推荐使用,原因已在前面提到。字段注入具有以下缺点:不清晰的依赖关系:依赖项通过字段注入,难以通过构造器或 setter 明确看到类的依赖。难以测试:无法通过构造函数直接注入模拟对象,增加了单元测试的难度。1234public class MyService { @Autowired private MyRepository repository;}4. 总结字段注入不推荐,因为它将依赖关系隐藏在字段中,难以清晰表达依赖项,增加了测试的复杂性。推荐使用构造器注入,它提供了最强的类型安全性和不可变性,增强了代码的可维护性和测试性。Setter 注入适用于可选依赖,但在依赖较多时容易导致依赖关系变得模糊,因此需要谨慎使用。总体而言,使用构造器注入和 setter 注入能够使代码更清晰、易于维护,同时支持更好的单元测试。如果 IDE 提示 Field injection is not recommended,这意味着你可以考虑改用构造器注入或 setter 注入,以便提升代码质量。
-
前言在 Java 中,将对象转换为 JSON 字符串通常使用一些流行的 JSON 库,如 Jackson 或 Gson。这两个库都非常强大,支持将 Java 对象转换为 JSON 字符串,也支持反向操作。接下来我会介绍一个基于 Jackson 的工具类,它可以非常方便地实现 Java 对象和 JSON 字符串之间的相互转换。1. 引入 Jackson 依赖首先,确保你的 pom.xml 文件中引入了 Jackson 相关依赖:12345678<dependencies> <!-- Jackson 核心库 --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> <!-- 使用合适的版本 --> </dependency></dependencies> 2. 创建 JSON 工具类以下是一个简单的 Jackson 工具类,实现了 Java 对象和 JSON 字符串之间的相互转换,并支持异常处理。 12345678910111213141516171819202122232425262728import com.fasterxml.jackson.databind.ObjectMapper;import com.fasterxml.jackson.core.JsonProcessingException; public class JsonUtils { // 创建一个 ObjectMapper 实例,用于转换对象 private static final ObjectMapper objectMapper = new ObjectMapper(); // 将对象转换为 JSON 字符串 public static String toJson(Object object) { try { return objectMapper.writeValueAsString(object); // 使用 ObjectMapper 将对象转成 JSON } catch (JsonProcessingException e) { e.printStackTrace(); // 打印异常 return null; // 返回 null 或者可以抛出自定义异常 } } // 将 JSON 字符串转换为对象 public static <T> T fromJson(String jsonString, Class<T> valueType) { try { return objectMapper.readValue(jsonString, valueType); // 使用 ObjectMapper 将 JSON 转回对象 } catch (JsonProcessingException e) { e.printStackTrace(); // 打印异常 return null; // 返回 null 或者可以抛出自定义异常 } }} 3. 使用示例转换 Java 对象为 JSON 字符串假设你有一个 Java 类 Person,并希望将其转换为 JSON 字符串。然后你可以使用上述的这个 JsonUtils 工具类来将 Person 对象转换为 JSON 字符串: 12345678910public class Main { public static void main(String[] args) { Person person = new Person("Alice", 30); // 将对象转换为 JSON 字符串 String json = JsonUtils.toJson(person); System.out.println(json); // 输出: {"name":"Alice","age":30} }} 将 JSON 字符串转换回 Java 对象12345678910public class Main { public static void main(String[] args) { String json = "{\"name\":\"Alice\",\"age\":30}"; // 将 JSON 字符串转换为 Person 对象 Person person = JsonUtils.fromJson(json, Person.class); System.out.println(person.getName()); // 输出: Alice System.out.println(person.getAge()); // 输出: 30 }} 4. 扩展:自定义序列化和反序列化Jackson 提供了强大的自定义序列化和反序列化功能。如果你有特殊的需求,可以通过注解或自定义 Serializer 和 Deserializer 来实现。例如,假设你想控制 Person 对象的 JSON 输出格式,可以使用 @JsonFormat 注解:12345678910import com.fasterxml.jackson.annotation.JsonFormat; public class Person { private String name; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") private int age; // 构造器,getter 和 setter} 这样,你可以根据需要灵活地调整 JSON 的生成方式。总结通过使用 Jackson 的 ObjectMapper,你可以轻松地将 Java 对象和 JSON 字符串之间进行转换。上述这个工具类提供了基本的功能,也支持更多复杂的自定义配置,适应不同的需求。
-
异步与同步的核心区别同步调用:调用方阻塞等待结果返回异步调用:调用方立即返回,通过回调/轮询等方式获取结果本文重点讨论如何将异步调用转为同步阻塞模式,以下是五种实现方案:方法一:使用wait/notify + synchronized public class ProducerConsumerExample { private static final int BUFFER_SIZE = 5; private final Object lock = new Object(); private int[] buffer = new int[BUFFER_SIZE]; private int count = 0; // 生产者线程 public void produce() throws InterruptedException { int value = 0; while (true) { synchronized (lock) { while (count == BUFFER_SIZE) { System.out.println("缓冲区已满,生产者等待..."); lock.wait(); } buffer[count++] = value++; System.out.println("生产数据: " + value + ",缓冲区数量: " + count); lock.notify(); } Thread.sleep(1000); } } // 消费者线程 public void consume() throws InterruptedException { while (true) { synchronized (lock) { while (count == 0) { System.out.println("缓冲区为空,消费者等待..."); lock.wait(); } int value = buffer[--count]; System.out.println("消费数据: " + value + ",缓冲区数量: " + count); lock.notify(); } Thread.sleep(1500); } } public static void main(String[] args) { ProducerConsumerExample example = new ProducerConsumerExample(); // 启动生产者和消费者线程 new Thread(example::produce).start(); new Thread(example::consume).start(); } }关键要点共享资源保护:通过synchronized(lock)保证线程安全条件判断:while循环而非if防止虚假唤醒缓冲区满时生产者等待(wait())缓冲区空时消费者等待(wait())协作机制:每次操作后通过notify()唤醒等待线程方法对比:notify():唤醒单个等待线程notifyAll():唤醒所有等待线程(适用于多生产者场景)方法二:使用ReentrantLock + Condition import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; public class TestReentrantLock4 { static ReentrantLock lock = new ReentrantLock(); static Condition moneyCondition = lock.newCondition(); static Condition ticketCondition = lock.newCondition(); static boolean haveMoney = false; static boolean haveTicket = false; public static void main(String[] args) throws InterruptedException { // 农民1(等钱) new Thread(() -> { lock.lock(); try { while (!haveMoney) { System.out.println("农民1等待资金..."); moneyCondition.await(); } System.out.println("农民1获得资金,回家!"); } finally { lock.unlock(); } }, "Farmer1").start(); // 农民2(等票) new Thread(() -> { lock.lock(); try { while (!haveTicket) { System.out.println("农民2等待车票..."); ticketCondition.await(); } System.out.println("农民2获得车票,回家!"); } finally { lock.unlock(); } }, "Farmer2").start(); // 主线程模拟发放条件 Thread.sleep(1000); lock.lock(); try { haveMoney = true; moneyCondition.signal(); System.out.println("资金已发放!"); haveTicket = true; ticketCondition.signal(); System.out.println("车票已发放!"); } finally { lock.unlock(); } } }核心特性多条件支持:一个锁对象可绑定多个Condition(如moneyCondition/ticketCondition)精准唤醒:await():释放锁并等待特定条件signal():唤醒满足条件的等待线程代码结构:必须在lock.lock()和finally unlock()之间操作条件判断使用while循环防止虚假唤醒方法三:Future(Callable + ExecutorService) import java.util.concurrent.*; public class FutureExample { public static void main(String[] args) { ExecutorService executor = Executors.newSingleThreadExecutor(); Future<Integer> future = executor.submit(() -> { int sum = 0; for (int i = 1; i <= 100; i++) { sum += i; Thread.sleep(10); } return sum; }); System.out.println("主线程执行其他任务..."); try { Integer result = future.get(2, TimeUnit.SECONDS); System.out.println("计算结果: 1+2+...+100 = " + result); } catch (TimeoutException e) { System.err.println("计算超时!"); future.cancel(true); } catch (Exception e) { e.printStackTrace(); } finally { executor.shutdown(); } } }关键API方法作用future.get()阻塞获取结果(可设置超时)future.cancel()取消任务执行isDone()检查任务是否完成执行流程提交Callable任务到线程池主线程继续执行其他操作调用future.get()阻塞等待结果处理可能出现的异常情况最终关闭线程池资源
-
引言在电缆行业,生成供货清单是一项常见但繁琐的任务。本教程将介绍如何使用现代Java技术栈自动化这一过程,大幅提高工作效率和准确性。我们将使用SpringBoot作为框架,Apache POI处理Word文档,以及FreeMarker作为模板引擎来实现这一功能!让我们先了解一下这个问题的背景:在电缆行业,手动创建供货清单是一个复杂且重复的过程。这个过程不仅耗时,还容易出错,影响工作效率和数据准确性。为了解决这个问题,我们提出了一个技术方案,结合了以下几个关键技术:SpringBoot: 作为我们的主要开发框架Apache POI: 用于生成和操作Word文档FreeMarker模板引擎: 用于生成Word文件的内容这个方案的主要优势包括:灵活性: 使用FreeMarker模板可以轻松调整文档格式,而无需修改程序代码。效率: 自动化生成过程大大减少了人工操作,提高了办公效率。准确性: 自动化处理确保了数据的准确性和一致性。适用性: 特别适合电缆行业的业务需求,生成符合要求的.doc文件。通过阅读这篇博客,您将学习如何实现这个解决方案,从而帮助您或您的团队简化工作流程,提高生产效率。效果图: 项目结构src/├── main/│ ├── java/│ │ └── com/│ │ └── pw/│ │ ├── WordController.java #负责生成测试数据并调用WordUtil工具类来生成Word文档│ │ └── utils/│ │ └── WordUtil.java #这个工具类封装了使用FreeMarker生成Word文档的核心功能│ └── resources/│ └── templates/│ └── template.ftl #模版定义了Word文档的结构和样式,使用HTML和CSS来格式化内容1.WordController类:这个类是我们应用的入口点,负责生成测试数据并调用WordUtil来生成Word文档。2.WordUtil类:这个工具类封装了使用FreeMarker生成Word文档的核心逻辑。3.FreeMarker模版(template.ftl):这个模版定义了Word文档的结构和样式,使用HTML和CSS来格式化内容。源代码展示1.WordControllerimport com.pw.utils.WordUtil; import java.io.File;import java.io.IOException;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map; public class WordController { public static void main(String[] args) throws IOException { // 指定保存Word文件的目录 String filePath = "F:\\Poi2Word\\src\\main\\resources\\output"; // 更改为您希望的目录 new WordController().generateWordFile(filePath); } public void generateWordFile(String directory) throws IOException { List<Map<String, Object>> listMap = new ArrayList<>(); //测试数据 addTestData(listMap, "4600025747", "绝缘导线", "AC10kV,JKLYJ,300", 1500, "米", "盘号:A1"); addTestData(listMap, "4600025748", "绝缘导线", "AC10kV,JKLGYJ,150/30", 2500, "米", "盘号:A2"); addTestData(listMap, "4600025749", "绝缘导线", "AC10kV,JKLGYJ,150/30", 3500, "米", "盘号:A3"); addTestData(listMap, "4600025750", "绝缘导线", "AC10kV,JKLGYJ,150/30", 4500, "米", "盘号:A4"); addTestData(listMap, "4600025751", "绝缘导线", "AC10kV,JKLGYJ,150/30", 3800, "米", "盘号:A5"); addTestData(listMap, "4600025752", "绝缘导线", "AC10kV,JKLYJ,180", 2000, "米", "盘号:A6"); addTestData(listMap, "4600025753", "绝缘导线", "AC10kV,JKLYJ,120", 4200, "米", "盘号:A7"); addTestData(listMap, "4600025754", "绝缘导线", "AC10kV,JKLYJ,120", 3700, "米", "盘号:A8"); addTestData(listMap, "4600025755", "绝缘导线", "AC10kV,JKLYJ,120", 4300, "米", "盘号:A9"); addTestData(listMap, "4600025756", "绝缘导线", "AC10kV,JKLGYJ,100/20", 2800, "米", "盘号:A10"); addTestData(listMap, "4600025757", "绝缘导线", "AC10kV,JKLGYJ,100/20", 2400, "米", "盘号:A11"); addTestData(listMap, "4600025758", "绝缘导线", "AC10kV,JKLGYJ,100/20", 2600, "米", "盘号:A12"); HashMap<String, Object> map = new HashMap<>(); map.put("qdList", listMap); // 添加供货清单数据 map.put("contacts", "张三"); // 联系人 map.put("contactsPhone", "13988887777"); // 联系电话 map.put("date", "2025年01月18日"); // 日期 map.put("company", "新电缆科技有限公司"); // 公司名称 map.put("customer", "国网北京市电力公司"); // 客户 String wordName = "template.ftl"; // FreeMarker模板文件名 String fileName = "供货清单" + System.currentTimeMillis() + ".doc"; // 带时间戳的文件名 String name = "name"; // 临时文件名 // 确保输出目录存在 File directoryFile = new File(directory); if (!directoryFile.exists()) { directoryFile.mkdirs(); // 如果目录不存在则创建 } // 生成Word文件 WordUtil.exportMillCertificateWord(directory, map, wordName, fileName, name); System.out.println("文件成功生成在:" + directory + fileName); } private void addTestData(List<Map<String, Object>> listMap, String danhao, String name, String model, int num, String unit, String remark) { Map<String, Object> item = new HashMap<>(); item.put("serNo", listMap.size() + 1); // 序号 item.put("danhao", danhao); // 单号 item.put("name", name); // 产品名称 item.put("model", model); // 规格型号 item.put("num", String.valueOf(num)); // 数量,转换为字符串 item.put("unit", unit); // 单位 item.put("remark", remark); // 备注 listMap.add(item); // 将数据添加到列表 }}2.WordUtil工具类package com.pw.utils; import freemarker.template.Configuration;import freemarker.template.Template; import java.io.*;import java.util.Map; public class WordUtil { private static Configuration configuration = null; // 模板文件夹路径 private static final String templateFolder = WordUtil.class.getResource("/templates").getPath(); static { configuration = new Configuration(); configuration.setDefaultEncoding("utf-8"); try { System.out.println(templateFolder); configuration.setDirectoryForTemplateLoading(new File(templateFolder)); // 设置模板加载路径 } catch (IOException e) { e.printStackTrace(); } } private WordUtil() { throw new AssertionError(); // 防止实例化 } /** * 导出Word文档 * @param map Word文档中参数 * @param wordName 模板的名字,例如xxx.ftl * @param fileName Word文件的名字 格式为:"xxxx.doc" * @param outputDirectory 输出文件的目录路径 * @param name 临时的文件夹名称,作为Word文件生成的标识 * @throws IOException */ public static void exportMillCertificateWord(String outputDirectory, Map map, String wordName, String fileName, String name) throws IOException { Template freemarkerTemplate = configuration.getTemplate(wordName); // 获取模板文件 File file = null; try { // 调用工具类的createDoc方法生成Word文档 file = createDoc(map, freemarkerTemplate, name); // 确保输出目录存在 File dir = new File(outputDirectory); if (!dir.exists()) { dir.mkdirs(); // 如果目录不存在则创建 } // 定义完整的文件路径 File outputFile = new File(outputDirectory, fileName); // 重命名并移动文件到指定目录 file.renameTo(outputFile); System.out.println("文件成功生成在: " + outputFile.getAbsolutePath()); } finally { if (file != null && file.exists()) { file.delete(); // 删除临时文件 } } } private static File createDoc(Map<?, ?> dataMap, Template template, String name) { File f = new File(name); try { // 使用OutputStreamWriter来指定编码,防止特殊字符出问题 Writer w = new OutputStreamWriter(new FileOutputStream(f), "utf-8"); template.process(dataMap, w); // 使用FreeMarker处理模板 w.close(); } catch (Exception ex) { ex.printStackTrace(); throw new RuntimeException(ex); } return f; // 返回生成的文件 }}3.FreeMarker模版<!DOCTYPE html><html><head> <meta charset="UTF-8"> <title>${company}送货清单</title> <style> body { font-family: SimSun, serif; } <!-- 设置字体 --> table { border-collapse: collapse; width: 100%; } <!-- 设置表格样式 --> th, td { border: 1px solid black; padding: 5px; text-align: center; } <!-- 设置表格的单元格样式 --> th { background-color: #f2f2f2; } <!-- 设置表头背景色 --> .subtotal { font-weight: bold; } <!-- 小计行加粗 --> .total { font-weight: bold; font-size: 1.1em; } <!-- 总计行加粗并设置字体大小 --> </style></head><body><h1 style="text-align: center;">${company}送货清单</h1> <!-- 顶部公司名称 --> <table> <tr> <th>序号</th> <!-- 表头:序号 --> <th>供货单号</th> <!-- 表头:供货单号 --> <th>产品名称</th> <!-- 表头:产品名称 --> <th>规格型号</th> <!-- 表头:规格型号 --> <th>数量</th> <!-- 表头:数量 --> <th>单位</th> <!-- 表头:单位 --> <th>备注</th> <!-- 表头:备注 --> </tr> <#assign totalQuantity = 0> <!-- 总数量初始化 --> <#assign totalItems = 0> <!-- 总项数初始化 --> <#assign sortedList = qdList?sort_by("model")> <!-- 按照规格型号排序 --> <#assign currentModel = ""> <!-- 当前型号初始化 --> <#assign subtotalQuantity = 0> <!-- 小计数量初始化 --> <#assign subtotalItems = 0> <!-- 小计项数初始化 --> <#list sortedList as item> <!-- 遍历排序后的列表 --> <#if item.model != currentModel> <!-- 如果规格型号变了 --> <#if currentModel != ""> <!-- 如果当前规格型号不是空 --> <tr class="subtotal"> <td colspan="4">小计:${subtotalQuantity}${sortedList[0].unit} ${subtotalItems}轴</td> <td>${subtotalQuantity}</td> <td>${sortedList[0].unit}</td> <td></td> </tr> </#if> <#assign currentModel = item.model> <!-- 更新当前型号 --> <#assign subtotalQuantity = 0> <!-- 重置小计数量 --> <#assign subtotalItems = 0> <!-- 重置小计项数 --> </#if> <tr> <td>${item?counter}</td> <!-- 序号 --> <td>${item.danhao}</td> <!-- 单号 --> <td>${item.name}</td> <!-- 产品名称 --> <td>${item.model}</td> <!-- 规格型号 --> <td>${item.num}</td> <!-- 数量 --> <td>${item.unit}</td> <!-- 单位 --> <td>${item.remark}</td> <!-- 备注 --> </tr> <#assign itemNum = item.num?replace(",", "")?number> <!-- 将数量转为数字并处理逗号 --> <#assign subtotalQuantity = subtotalQuantity + itemNum> <!-- 累加小计数量 --> <#assign subtotalItems = subtotalItems + 1> <!-- 累加小计项数 --> <#assign totalQuantity = totalQuantity + itemNum> <!-- 累加总数量 --> <#assign totalItems = totalItems + 1> <!-- 累加总项数 --> </#list> <#if currentModel != ""> <!-- 如果当前规格型号不是空 --> <tr class="subtotal"> <td colspan="4">小计:${subtotalQuantity}${sortedList[0].unit} ${subtotalItems}轴</td> <td>${subtotalQuantity}</td> <td>${sortedList[0].unit}</td> <td></td> </tr> </#if> <tr class="total"> <td colspan="4">合计:${totalQuantity}${qdList[0].unit} ${totalItems}轴</td> <td>${totalQuantity}</td> <td>${qdList[0].unit}</td> <td></td> </tr></table> <p>发货联系人:${contacts}</p> <!-- 发货联系人 --><p>联系电话:${contactsPhone}</p> <!-- 联系电话 --><p>日期:${date}</p> <!-- 日期 --> <p style="text-align: right;">收货人(签字):_______________</p> <!-- 收货人签字 --><p style="text-align: right;">联系电话:_______________</p> <!-- 收货人联系电话 --><p style="text-align: right;">${customer}</p> <!-- 客户 --></body></html>4.POM依赖<!-- freemarker依赖,用于模板引擎,方便进行页面的渲染和数据的展示等操作 --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId></dependency><!-- Apache POI 的核心依赖,用于操作 Microsoft Office 格式的文档,如 Excel、Word 等文件 --><dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>5.0.0</version></dependency><!-- Apache POI 的 OOXML 扩展依赖,主要用于处理 Office 2007 及以后版本的 OOXML 格式的文件,例如.xlsx 等 --><dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>5.0.0</version></dependency><!-- OOXML 模式相关的依赖,提供了对 OOXML 文档结构和内容模式的支持,有助于 Apache POI 更好地操作 OOXML 格式文件 --><dependency> <groupId>org.apache.poi</groupId> <artifactId>ooxml-schemas</artifactId> <version>1.4</version></dependency>WordController类深度解析WordController类是整个应用的核心控制器,负责协调数据生成和文档创建的过程。让我们逐步分析它的主要组成部分:1.类结构public class WordController { // 方法定义...}这个类没有继承任何其他类,也没有实现任何接口,是一个独立的控制器类。2.main方法public static void main(String[] args) throws IOException { String filePath = "F:\\Poi2Word\\src\\main\\resources\\output"; new WordController().generateWordFile(filePath);}这是应用的入口点。它设置了输出文件的路径,然后调用generateWordFile方法。请注意:在常规的 Spring Boot 实际应用场景下,我们一般不会直接在控制器类中使用 main 方法。此处之所以将 main 方法置于控制器中,纯粹是出于演示目的,旨在让相关流程更加直观易懂。而当进入到正式开发环节时,有几个关键要点务必落实:其一,需要引入数据库集成功能,将当前所使用的测试数据全面替换为从数据库中精准查询获取的真实数据,以此确保数据的准确性与时效性;其二,要对控制器进行优化改造,摒弃现有的演示模式,将其转换为遵循标准规范的请求接口实现方式,进而满足实际业务需求,提升系统的稳定性与可扩展性。3.generateWordFile方法此方法的只要目的是生成Word文件,首先需要先收集和存储测试数据,存储表格数据是将一条数据存储在Map集合中,再将每一条数据存储到List集合中。将其他数据存储到单独的一个Map集合中。然后确保输出目录存在,最后调用WordUtil中的exportMillCertificateWord方法生成文件,并输出文件的生成位置。// 生成 Word 文件的方法public void generateWordFile(String directory) throws IOException { // 存储测试数据的列表,每个元素都是一个 Map,存储了具体的信息 List<Map<String, Object>> listMap = new ArrayList<>(); // 添加测试数据,调用 addTestData 方法添加一条记录 addTestData(listMap, "4600025747", "绝缘导线", "AC10kV,JKLYJ,300", 1500, "米", "盘号:A1"); //... 可以继续调用 addTestData 方法添加更多测试数据... // 存储最终要填充到 Word 模板的数据的 Map,包含各种信息 HashMap<String, Object> map = new HashMap<>(); // 将测试数据列表添加到 map 中,键为 "qdList" map.put("qdList", listMap); // 联系人信息 map.put("contacts", "张三"); // 联系人电话 map.put("contactsPhone", "13988887777"); // 日期信息 map.put("date", "2025年01月18日"); // 公司名称 map.put("company", "新电缆科技有限公司"); // 客户名称 map.put("customer", "国网北京市电力公司"); // Word 模板文件的名称 String wordName = "template.ftl"; // 生成的 Word 文件的名称,使用当前时间戳保证文件名的唯一性 String fileName = "供货清单" + System.currentTimeMillis() + ".doc"; // 名称信息,具体含义可能根据实际情况而定 String name = "name"; // 创建一个文件对象,用于表示输出目录 File directoryFile = new File(directory); // 检查输出目录是否存在,如果不存在则创建目录 if (!directoryFile.exists()) { directoryFile.mkdirs(); } // 调用 WordUtil 的 exportMillCertificateWord 方法生成 Word 文件 // 传入目录、数据 Map、模板名称、生成的文件名称和名称信息 WordUtil.exportMillCertificateWord(directory, map, wordName, fileName, name); // 打印生成文件的成功信息 System.out.println("文件成功生成在:" + directory + fileName);}这个方法完成以下任务:创建一个一个List<Map<String,Object>>集合来存储供货清单数据使用addTestData方法添加多条测试数据创建一个Map集合来存储企业名称,发货联系人,联系电话等信息确保输出目录存在调用WordUtil.exportMillCertificateWord方法来生成Word文档4.addTestData方法这个方法用于创建单个供货项目的数据// 添加一条测试数据到 listMap 中private void addTestData(List<Map<String, Object>> listMap, String danhao, String name, String model, int num, String unit, String remark) { // 创建一个新的 HashMap,用于存储每一条数据 Map<String, Object> item = new HashMap<>(); // 将数据项依次放入 HashMap 中,"serNo" 表示序号,使用 listMap 的大小+1 生成序号 item.put("serNo", listMap.size() + 1); // 序号是当前列表的大小 + 1 item.put("danhao", danhao); // 供货单号 item.put("name", name); // 产品名称 item.put("model", model); // 规格型号 item.put("num", String.valueOf(num)); // 数量,将整数转为字符串 item.put("unit", unit); // 单位 item.put("remark", remark); // 备注 // 将该条数据项添加到 listMap 列表中 listMap.add(item);}这个方法完成以下任务:它接收多个参数,代表一个供货项目的各个属性。创建一个新的Map来存储这个项目的数据。自动计算序号(serNo)基于当前列表的大小。将所有数据添加到Map中。将这个Map添加到供货清单列表中。WordUtil类深度解析WordUtil类是整个文档生成过程的核心,它封装了FreeMarker模板引擎的配置和使用逻辑。让我们逐步分析它的主要组成部分:1.类结构和静态成员public class WordUtil { private static Configuration configuration = null; private static final String templateFolder = WordUtil.class.getResource("/templates").getPath(); // 其他方法...}configuration:这是FreeMarker的核心配置对象,用于设置模版加载路径。templateFolder:定义了模版文件的存储路径。使用getResource()方法确保在不同环境下都能正确找到模版文件。2.静态初始化块这段代码的作用是初始化FreeMarker的Configuration对象,设置模版加载目录以及编码格式,以便FreeMarker后续能够正确加载和处理模版文件。// 静态初始化块,用于初始化 FreeMarker 配置static { // 创建一个 FreeMarker 配置对象,用于后续模板处理 configuration = new Configuration(); // 设置 FreeMarker 配置对象的默认编码为 "utf-8" configuration.setDefaultEncoding("utf-8"); try { // 输出模板文件夹路径,帮助调试 System.out.println(templateFolder); // 设置模板加载目录为 templateFolder 指定的路径,模板文件会从该目录加载 configuration.setDirectoryForTemplateLoading(new File(templateFolder)); } catch (IOException e) { // 如果加载模板目录时出现异常,打印错误堆栈信息 e.printStackTrace(); }}这个静态初始化块在类加载时执行,主要完成以下任务:创建FreeMarker的Configuration对象设置默认编码为UTF-8,确保正确处理中文等字符设置模版加载目录,这样FreeMarker就知道从哪里查找加载模版文件了错误处理:如果执行过程中出现了IO异常,就会打印堆栈跟踪3.私有构造函数这个构造函数防止类被实例化,确保WordUtil只能通过其静态方法使用。private WordUtil() { throw new AssertionError();}私有构造函数的好处包括:防止类被实例化当类的构造函数被声明为private时,外部代码无法直接创建该类的实例。这就意味着该类只能公国静态方法访问,确保类的功能是全局共享的。实现单例模式的基础在一些设计模式中,例如单例模式,类只允许有一个实例,私有构造函数确保了这一点。通过private构造函数,我们可以控制类的实例化过程,并确保只有一个实例被创建。封装类的内部实现私有构造函数可以帮助隐藏类的具体实现细节,外部代码不需要关心如何创建类的实例,只需要使用类提供的静态方法即可。这增加了类的封装性,降低了与外部代码的耦合度。避免多余的对象创建由于无法实例化类,每次调用静态方法时,都会使用已有的类实例,这可以避免无意义的对象创建,节省内存和资源。4.exportMillCertificateWord方法这个方法的主要功能是通过加载指定的 FreeMarker 模板生成一个临时的 Word 文档,确保输出目录存在后,将临时文件重命名并保存到指定的位置,同时在过程结束后清理临时文件,并打印文件生成的成功消息。// 导出 Word 文档的方法public static void exportMillCertificateWord(String outputDirectory, Map map, String wordName, String fileName, String name) throws IOException { // 获取 FreeMarker 模板文件 Template freemarkerTemplate = configuration.getTemplate(wordName); // 初始化一个 File 对象,用于存储生成的临时文件 File file = null; try { // 使用模板和数据创建 Word 文档,返回临时文件 file = createDoc(map, freemarkerTemplate, name); // 创建目标目录的 File 对象 File dir = new File(outputDirectory); // 如果目录不存在,则创建该目录 if (!dir.exists()) { dir.mkdirs(); // 创建目录及其父目录 } // 定义最终输出文件的完整路径(包括目录和文件名) File outputFile = new File(outputDirectory, fileName); // 将临时生成的文件重命名为目标文件,并将其移动到指定目录 file.renameTo(outputFile); // 打印输出文件的绝对路径,a通知文件生成成功 System.out.println("文件成功生成在: " + outputFile.getAbsolutePath()); } finally { // 最后,无论是否成功生成文件,都确保临时文件被删除 if (file != null && file.exists()) { file.delete(); // 删除临时文件 } }}这个方法是文档导出的主要入口,主要实现了以下功能:加载指定的FreeMarker模版调用createDoc方法生成临时文档文件确保输出目录存在将临时文件重命名并移动到指定的输出位置使用finally块确保临时文件被删除,无论过程是否成功5.createDoc方法这个方法是创建文档的核心方法,主要是通过创建一个临时文件,使用指定的FreeMarker模版和数据模型将内容填充到文件中,并确保文件使用UTF-8编码进行写入。该方法在执行过程中捕获异常并打印堆栈信息,确保发生错误时能够正确处理。最后。方法返回生成的文件对象,以便后续操作或保存。// 创建文档的方法,使用 FreeMarker 模板生成内容并写入文件private static File createDoc(Map<?, ?> dataMap, Template template, String name) { // 创建一个新的 File 对象,表示生成的文档文件,文件名由参数 "name" 提供 File f = new File(name); try { // 使用 OutputStreamWriter 创建一个写入文件的 Writer 对象,设置编码为 "utf-8" Writer w = new OutputStreamWriter(new FileOutputStream(f), "utf-8"); // 使用 FreeMarker 模板将数据填充到文件中 template.process(dataMap, w); // 关闭 Writer,确保所有内容写入文件 w.close(); } catch (Exception ex) { // 捕获异常并打印错误堆栈信息 ex.printStackTrace(); // 抛出 RuntimeException,确保错误被传播到调用者 throw new RuntimeException(ex); } // 返回生成的文件对象 return f;}这个方法是实际创建文档的核心,主要实现以下功能:创建一个临时文件。使用OutputStreamWriter设置UTF-8编码,确保正确处理所有字符。调用FreeMarker的template.process()方法,将数据模型(dataMap)应用到模板上。关闭写入器。如果过程中发生异常,打印堆栈跟踪并抛出RuntimeException。返回生成的文件对象。6.WordUtil类总结WordUtil 类通过封装 FreeMarker 模板引擎的配置和文件操作,提供了一个简洁的文档生成工具。它加载指定模板,使用数据模型填充内容,创建临时文件,并确保文件按照指定路径保存。该类通过静态方法确保全局共享功能,使用 UTF-8 编码处理字符,捕获异常并清理临时文件,确保文档生成过程的稳定性和高效性。FreeMarker模板深度解析FreeMarker模板是整个文档生成过程的核心,它定义了最终Word文档的结构和样式。让我们来逐步分析模板的主要组成部分1.文档结构和样式<!DOCTYPE html> <!-- 声明文档类型为 HTML5 --><html><head> <!-- 设置文档字符编码为 UTF-8,支持中文和其他字符集 --> <meta charset="UTF-8"> <!-- 设置页面标题,动态插入公司名称 --> <title>${company}送货清单</title> <style> /* 设置页面正文的字体为 SimSun(宋体),如果没有则使用 serif */ body { font-family: SimSun, serif; } /* 设置表格样式:表格边框合并,宽度100% */ table { border-collapse: collapse; width: 100%; } /* 设置表格头部和单元格的边框、内边距和文本居中对齐 */ th, td { border: 1px solid black; padding: 5px; text-align: center; } /* 设置表头背景色为浅灰色 */ th { background-color: #f2f2f2; } /* 设置小计行字体加粗 */ .subtotal { font-weight: bold; } /* 设置合计行字体加粗,字体大小稍大 */ .total { font-weight: bold; font-size: 1.1em; } </style></head><body> <!-- 页面标题,居中显示公司名称和送货清单 --> <h1 style="text-align: center;">${company}送货清单</h1> <!-- 表格内容将在这里生成,动态插入数据 --></body></html>这段代码通过HTML和内嵌CSS定义了页面布局和样式:动态公司名称:<title>标签使用${company}插入动态的公司名称,显示在浏览器标签中。字体和表格样式:设置页面字体为宋体(Simsun)定义表格边框合并、100%宽度,并使单元格内容居中小计和总计行样式:为小计行加粗字体,并为总计行加粗且增大字体,突出显示重要数据。2.表格结构和动态数据插入<table> <!-- 表头,定义表格的列名 --> <tr> <th>序号</th> <!-- 序号 --> <th>供货单号</th> <!-- 供货单号 --> <th>产品名称</th> <!-- 产品名称 --> <th>规格型号</th> <!-- 规格型号 --> <th>数量</th> <!-- 数量 --> <th>单位</th> <!-- 单位 --> <th>备注</th> <!-- 备注 --> </tr> <!-- 初始化总计和小计相关变量 --> <#assign totalQuantity = 0> <!-- 总数量 --> <#assign totalItems = 0> <!-- 总项数 --> <#assign sortedList = qdList?sort_by("model")> <!-- 按照规格型号对数据进行排序 --> <#assign currentModel = ""> <!-- 当前规格型号 --> <#assign subtotalQuantity = 0> <!-- 小计数量 --> <#assign subtotalItems = 0> <!-- 小计项数 --> <!-- 遍历排序后的列表 --> <#list sortedList as item> <!-- 如果当前项的规格型号与上一项不同,则输出上一项的小计 --> <#if item.model != currentModel> <#if currentModel != ""> <!-- 输出上一规格型号的小计行 --> <tr class="subtotal"> <td colspan="4">小计:${subtotalQuantity}${sortedList[0].unit} ${subtotalItems}轴</td> <td>${subtotalQuantity}</td> <td>${sortedList[0].unit}</td> <td></td> </tr> </#if> <!-- 更新当前规格型号为当前项的规格型号,并重置小计 --> <#assign currentModel = item.model> <#assign subtotalQuantity = 0> <#assign subtotalItems = 0> </#if> <!-- 输出当前行数据 --> <tr> <td>${item?counter}</td> <!-- 序号,使用 FreeMarker 的 counter 计数 --> <td>${item.danhao}</td> <!-- 供货单号 --> <td>${item.name}</td> <!-- 产品名称 --> <td>${item.model}</td> <!-- 规格型号 --> <td>${item.num}</td> <!-- 数量 --> <td>${item.unit}</td> <!-- 单位 --> <td>${item.remark}</td> <!-- 备注 --> </tr> <!-- 更新小计和总计的数量和项数 --> <#assign itemNum = item.num?replace(",", "")?number> <!-- 将数量转为数字并处理逗号 --> <#assign subtotalQuantity = subtotalQuantity + itemNum> <!-- 累加小计数量 --> <#assign subtotalItems = subtotalItems + 1> <!-- 累加小计项数 --> <#assign totalQuantity = totalQuantity + itemNum> <!-- 累加总数量 --> <#assign totalItems = totalItems + 1> <!-- 累加总项数 --> </#list> <!-- 如果最后一项有数据,输出最后的规格型号小计 --> <#if currentModel != ""> <tr class="subtotal"> <td colspan="4">小计:${subtotalQuantity}${sortedList[0].unit} ${subtotalItems}轴</td> <td>${subtotalQuantity}</td> <td>${sortedList[0].unit}</td> <td></td> </tr> </#if> <!-- 输出最终的合计行 --> <tr class="total"> <td colspan="4">合计:${totalQuantity}${qdList[0].unit} ${totalItems}轴</td> <!-- 显示合计的数量和项数 --> <td>${totalQuantity}</td> <!-- 合计数量 --> <td>${qdList[0].unit}</td> <!-- 单位 --> <td></td> </tr></table>表格结构:使用 <table> 标签创建表格,并通过 <th> 定义表头,包含7列:序号、供货单号、产品名称等。动态数据插入:使用 FreeMarker <#list> 遍历排序后的清单数据,并通过 ${item.属性名} 动态插入每项数据,如 ${item.danhao} 插入供货单号。小计和总计计算:通过 <#assign> 定义变量如 totalQuantity 和 subtotalQuantity,在循环中累加数量。使用 <#if> 判断条件,插入小计行,并在循环结束后插入总计行。数据处理:使用 sortedList = qdList?sort_by("model") 按型号对清单数据进行排序。处理数量 itemNum = item.num?replace(",", "")?number,移除逗号并转换为数字,确保计算正确。格式化输出:小计和总计行使用 colspan 属性合并单元格,确保表格显示整洁。使用 CSS 类 subtotal 和 total 为小计和总计行应用加粗和突出显示的样式。总结:此表格通过 FreeMarker 动态插入数据、计算小计和总计,并通过合适的排序和格式化样式,确保清单展示清晰且易于阅读。最后,模板还包括了一些额外信息:<p>发货联系人:${contacts}</p><p>联系电话:${contactsPhone}</p><p>日期:${date}</p> <p style="text-align: right;">收货人(签字):_______________</p><p style="text-align: right;">联系电话:_______________</p><p style="text-align: right;">${customer}</p>这部分添加了额外的联系信息和签名区域,进一步完善了文档的实用性。总的来,这个FreeMarker模板展示了如何结合HTML、CSS和FreeMarker的模板语法来创建一个复杂、动态且格式良好的文档。它不仅能够准确地呈现数据,还能执行必要的计算和格式化,从而生成一个专业的供货清单文档。总结通过使用SpingBoot、Apache POI和FreeMarker,我们成功自动化了电缆供货清单的生成过程。这不仅提高了效率,还减少了人为错误。本解决方案的模块化设计使其易于维护和扩展。希望本教程能够帮助您理解如何使用Java技术来解决实际业务问题。————————————————原文链接:https://blog.csdn.net/weixin_66401877/article/details/145230273
-
前言在计算机科学领域,数据结构是算法实现的基石,而二叉树则是其中一颗璀璨的明珠。它以独特的树形结构,在众多场景中发挥着关键作用。二叉树由节点组成,每个节点最多包含两个子节点,这种简洁而强大的设计,使得数据的存储、检索与处理变得高效且有序。无论是数据库索引、编译器的语法分析,还是人工智能中的决策树算法,都离不开二叉树的身影。在Java编程世界里,掌握二叉树的生成与操作是迈向高级编程的重要一步。通过使用Java语言来构建二叉树,不仅能够深入理解数据结构的底层原理,还能提升解决复杂问题的能力。接下来,我们将一步步深入探索如何在Java中实现二叉树,从节点的定义到树的构建,再到各种遍历与操作方法,揭开这一重要数据结构的神秘面纱。一:什么是二叉树二叉树是一种每个节点最多有两个子节点的树形数据结构,这两个子节点通常被称为左子节点和右子节点。这种简洁而强大的结构,在许多算法和数据处理场景中发挥着关键作用。树是⼀种⾮线性的数据结构,它是由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)棵互不相交的树组成的集合称为森林二:Java中二叉树的实现2.1定义二叉树节点类首先,我们需要定义一个类来表示二叉树的节点。每个节点包含一个数据元素,以及指向左子节点和右子节点的引用。class TreeNode { int data; TreeNode left; TreeNode right; TreeNode(int data) { this.data = data; this.left = null; this.right = null; }}2.2构建二叉树接下来,我们可以编写代码来构建一棵简单的二叉树。public class BinaryTreeMagic { public static void main(String[] args) { // 创建根节点 TreeNode root = new TreeNode(1); // 创建左子节点 root.left = new TreeNode(2); // 创建右子节点 root.right = new TreeNode(3); // 为左子节点创建左子节点 root.left.left = new TreeNode(4); // 为左子节点创建右子节点 root.left.right = new TreeNode(5); // 至此,一棵简单的二叉树构建完成 // 1 // / \ // 2 3 // / \ // 4 5 }}2.3二叉树的遍历遍历二叉树是对树中每个节点进行访问的过程。常见的遍历方式有三种:前序遍历、中序遍历和后序遍历。学习⼆叉树结构,最简单的⽅式就是遍历。所谓遍历(Traversal)是指沿着某条搜索路线,依次对树中每个结点均做⼀次且仅做⼀次访问。访问结点所做的操作依赖于具体的应⽤问题(⽐如:打印节点内容、节点内容加1)。 遍历是⼆叉树上最重要的操作之⼀,是⼆叉树上进⾏其它运算之基础。在遍历⼆叉树时,如果没有进⾏某种约定,每个⼈都按照⾃⼰的⽅式遍历,得出的结果就⽐较混乱,如果按照某种规则进⾏约定,则每个⼈对于同⼀棵树的遍历结果肯定是相同的。如果N代表根节点,L代表根节点的左⼦树,R代表根节点的右⼦树,则根据遍历根节点的先后次序有以下遍历⽅式:• NLR:前序遍历(Preorder Traversal 亦称先序遍历)⸺访问根结点—>根的左⼦树—>根的右⼦树。• LNR:中序遍历(Inorder Traversal)⸺根的左⼦树—>根节点—>根的右⼦树。• LRN:后序遍历(Postorder Traversal)⸺根的左⼦树—>根的右⼦树—>根节点2.3.1前序遍历前序遍历的顺序是先访问根节点,然后递归地访问左子树,最后递归地访问右子树。void preOrderTraversal(TreeNode node) { if (node!= null) { System.out.print(node.data + " "); preOrderTraversal(node.left); preOrderTraversal(node.right); }}2.3.2中序遍历中序遍历的顺序是先递归地访问左子树,然后访问根节点,最后递归地访问右子树。void inOrderTraversal(TreeNode node) { if (node!= null) { inOrderTraversal(node.left); System.out.print(node.data + " "); inOrderTraversal(node.right); }}2.3.3后序遍历后序遍历的顺序是先递归地访问左子树,然后递归地访问右子树,最后访问根节点。void postOrderTraversal(TreeNode node) { if (node!= null) { postOrderTraversal(node.left); postOrderTraversal(node.right); System.out.print(node.data + " "); }}前序遍历结果:1 2 3 4 5 6中序遍历结果:3 2 1 5 4 6后序遍历结果:3 2 5 6 4 12.3.4 层序遍历层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对⼆叉树进⾏层序遍历。设⼆叉树的根节点所在层数为1,层序遍历就是从所在⼆叉树的根节点出发,⾸先访问第⼀层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,⾃上⽽下,⾃左⾄右逐层访问树的结点的过程就是层序遍历。以这棵树为例写下面题目:1.某完全⼆叉树按层次输出(同⼀层从左到右)的序列为 ABCDEFGH 。该完全⼆叉树的前序序列为(A)A: ABDHECFGB: ABCDEFGHC: HDBEAFCGD: HDEBFGCA2.⼆叉树的先序遍历和中序遍历如下:先序遍历:EFHIGJK;中序遍历:HFIEJKG.则⼆叉树根结点为(A)A: EB: FC: GD: H3.设⼀课⼆叉树的中序遍历序列:badce,后序遍历序列:bdeca,则⼆叉树前序遍历序列为(D)A: adbceB: decabC: debacD: abcde4.某⼆叉树的后序遍历序列与中序遍历序列相同,均为 ABCDEF ,则按层次输出(同⼀层从左到右)的序列为(A)A: FEDCBAB: CBAFEDC: DEFCBAD: ABCDEF2.4完整代码示例下面是一个包含二叉树构建和遍历功能的完整Java代码示例:class TreeNode { int data; TreeNode left; TreeNode right; TreeNode(int data) { this.data = data; this.left = null; this.right = null; }}public class BinaryTreeMagic { void preOrderTraversal(TreeNode node) { if (node!= null) { System.out.print(node.data + " "); preOrderTraversal(node.left); preOrderTraversal(node.right); } } void inOrderTraversal(TreeNode node) { if (node!= null) { inOrderTraversal(node.left); System.out.print(node.data + " "); inOrderTraversal(node.right); } } void postOrderTraversal(TreeNode node) { if (node!= null) { postOrderTraversal(node.left); postOrderTraversal(node.right); System.out.print(node.data + " "); } } public static void main(String[] args) { BinaryTreeMagic tree = new BinaryTreeMagic(); // 创建根节点 TreeNode root = new TreeNode(1); // 创建左子节点 root.left = new TreeNode(2); // 创建右子节点 root.right = new TreeNode(3); // 为左子节点创建左子节点 root.left.left = new TreeNode(4); // 为左子节点创建右子节点 root.left.right = new TreeNode(5); System.out.println("前序遍历:"); tree.preOrderTraversal(root); System.out.println(); System.out.println("中序遍历:"); tree.inOrderTraversal(root); System.out.println(); System.out.println("后序遍历:"); tree.postOrderTraversal(root); System.out.println(); }}三:二叉树的应用场景搜索算法:二叉搜索树(BST)是一种特殊的二叉树,它满足左子树所有节点的值小于根节点的值,右子树所有节点的值大于根节点的值。这种特性使得在BST中进行搜索操作的时间复杂度为O(log n),大大提高了搜索效率。表达式求值:通过构建表达式二叉树,可以方便地对数学表达式进行求值。例如,对于表达式“(3 + 4) * 2”,可以构建相应的二叉树来进行计算。文件系统目录结构:可以用二叉树来模拟文件系统的目录结构,根节点表示根目录,子节点表示子目录或文件,方便进行文件管理和查找。⽂件系统管理(⽬录和⽂件)四:二叉树的重点4.1概念⼀棵⼆叉树是结点的⼀个有限集合,该集合:或者为空或者是由⼀个根节点加上两棵别称为左⼦树和右⼦树的⼆叉树组成从上图可以看出:1. ⼆叉树不存在度⼤于2的结点2. ⼆叉树的⼦树有左右之分,次序不能颠倒,因此⼆叉树是有序树 注意:对于任意的⼆叉树都是由以下⼏种情况复合⽽成的:4.2大自然的一些奇观 4.3两种特殊的⼆叉树满⼆叉树: ⼀棵⼆叉树,如果每层的结点数都达到最⼤值,则这棵⼆叉树就是满⼆叉树。也就是说,如果⼀棵⼆叉树的层数为K,且结点总数是 ,则它就是满⼆叉树。完全⼆叉树: 完全⼆叉树是效率很⾼的数据结构,完全⼆叉树是由满⼆叉树⽽引出来的。对于深度为K的,有n个结点的⼆叉树,当且仅当其每⼀个结点都与深度为K的满⼆叉树中编号从0⾄n-1的结点⼀ 对应时称之为完全⼆叉树。 要注意的是满⼆叉树是⼀种特殊的完全⼆叉树。4.4二叉树的一些性质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<n,左孩⼦序号:2i+1,否则⽆左孩⼦◦ 若2i+2<n,右孩⼦序号:2i+2,否则⽆右孩⼦下面一些题目为例:某⼆叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该⼆叉树中的叶⼦结点数为( )A 不存在这样的⼆叉树B 200C 198D 199答案:B2.在具有 2n 个结点的完全⼆叉树中,叶⼦结点个数为( )A nB n+1C n-1D n/2答案:A3.⼀个具有767个节点的完全⼆叉树,其叶⼦节点个数为()A 383B 384C 385D 386答案:B4.⼀棵完全⼆叉树的节点数为531个,那么这棵树的⾼度为( )A 11B 10C 8D 12答案:B五:总结Java二叉树就像代码世界里的神奇魔法棒,通过巧妙地构建和操作树形结构,为我们解决各种复杂的编程问题提供了有力的工具。无论是高效的搜索算法,还是复杂的表达式求值,二叉树都展现出了其独特的魅力和强大的功能。希望通过本文的介绍和代码示例,你能对Java二叉树有更深入的理解和认识,在编程的道路上运用这神奇的树形魔法创造出更多精彩的代码。可以已经准备好进一步探索二叉树的更多高级特性和应用,比如平衡二叉树、红黑树等,它们将为你的编程技能库增添更多强大的武器。————————————————原文链接:https://blog.csdn.net/2301_80350265/article/details/145692883
-
一.栈(Stack)1.1栈的概念栈:一种特殊的线性表,其 只允许在固定的一端进行插入和删除元素操作 。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO ( Last In First Out )的原则 压栈:栈的插入操作叫做进栈 / 压栈 / 入栈, 入数据在栈顶 。出栈:栈的删除操作叫做出栈。 出数据在栈顶 1.2栈的实现及模拟public class Test { public static void main(String[] args) { Stack<Integer>s=new Stack();//创建一个空栈 s.push(1);//往栈中存入1 s.push(2);//2 s.push(3);//3 s.push(4);//4 s.push(5);//5 System.out.println(s.size());//有效个数5 System.out.println(s.peek());//获取栈顶元素5 s.pop();//5出栈 System.out.println(s.peek());//此时栈顶元素变为4 System.out.println(s.empty());//判断是否为空栈,此时不为空 返回false }} 这里我们用自己的方法来模拟实现上述的方法public class MyStack { int[] elem; int usedSize; public MyStack(){ this.elem=new int[10]; } public void push(int val){ if(isFull()){ //扩容 elem= Arrays.copyOf(elem,elem.length*2); } elem[usedSize]=val; usedSize++; } public boolean isFull(){ return usedSize==elem.length; } public int pop(){ if(empty()){ return -1; } int oldVal=elem[usedSize-1]; usedSize--; return oldVal; } public int peek(){ if(empty()){ return -1; } return elem[usedSize-1]; } public boolean empty(){ return usedSize==0; }}二.队列(Queue)2.1队列的概念队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out) 入队列:进行插入操作的一端称为队尾(Tail/Rear) 出队列:进行删除操作的一端称为队头(Head/Front) 2.2队列的实现及模拟 在Java中,Queue是个接口,底层是通过链表实现的注意:Queue是个接口,在实例化时必须实例化LinkedList的对象,因为LinkedList实现了Queue接口。 public class Test{ public static void main(String[] args) { Queue<Integer>q=new LinkedList<>(); q.offer(1);//从队尾入 q.offer(2); q.offer(3); q.offer(4); System.out.println(q.size());//有效个数 4 System.out.println(q.peek());//获取头元素 1 q.poll();//1从队列中出 System.out.println(q.peek());//2 System.out.println(q.isEmpty());//此时队列不为空,所以返回 false }}这里我们进行模拟实现上述方法 public class MyQueue { static class ListNode{ public int val; public ListNode prev; public ListNode next; public ListNode(int val){ this.val=val; } } public ListNode head; public ListNode last; public void offer(int val){ ListNode node=new ListNode(val); if(head==null){ head=last=node; }else{ last.next=node; node.prev=last; last=last.next; } } public int poll(){ if(head==null){ return -1; } int ret=head.val; if(head.next==null){ head=last=null; }else{ head=head.next; head.prev=null; } return ret; } public int peek(){ if(head == null) { return -1; } return head.val; } public boolean isEmpty(){ return head==null; }} 2.3循环队列实际中我们有时还会使用一种队列叫循环队列。如操作系统课程讲解生产者消费者模型时可以就会使用循环队列。环形队列通常使用数组实现。 2.4双端队列(Deque)双端队列(deque)是指允许两端都可以进行入队和出队操作的队列,deque 是 “double ended queue” 的简称。那就说明元素可以从队头出队和入队,也可以从队尾出队和入队 Deque是一个接口,使用时必须创建LinkedList的对象 Deque<Integer> stack = new ArrayDeque<>();//双端队列的线性实现Deque<Integer> queue = new LinkedList<>();//双端队列的链式实现———————————————— 原文链接:https://blog.csdn.net/2301_80288511/article/details/137435796
-
关于前端和大家推荐一个书籍,就是JavaScript高级程序设计,也叫红宝书,内容非常全面详细,大家可以买来看,以后面试工作的时候可能会用到,知识点什么的讲解的都挺好的也比较全面1.首先讲解一下src和href的区别:1.src是source的缩写,表示对资源的引用,它指向的内容会嵌入到当前标签所在的位置,src会将其指向的资源下载并应用到文档当中,当浏览器解析到带有src属性的标签时,它会发起一个HTTP请求来加载指定的资源,并将其嵌入到文档中。例如,<img src="image.png">会使浏览器加载并显示图片,当浏览器解析到该元素时,会暂停其他资源的下载和处理,直到将该资源加载,编译,执行完毕,所以一半js脚本会放在页面底部。2.href是hypertext reference的缩写,表示超文本引用,它指向一些网络资源,建立和当前元素或本文档的链接关系,当浏览器是识别到它指向的文件时,就会并行下载资源,不会停止对当前文档的处理,常用在a,link,等标签上。当用户点击带有 href属性的链接时,浏览器会导航到指定的URL。例如,<a href="https://example.com">Visit Example</a>会创建一个链接,点击后将用户带到 "example.com"。主要区别:src用于嵌入或替换当前元素的内容,并且src会阻塞页面的解析和渲染,直到资源加载,处理完毕href用于建立链接关系,会并行下载资源,不会阻塞页面的解析和渲染2.HTML语义化语义化是指内容的结构化(内容语义化),选择合适的标签(代码语义化)内容语义化:内容语义化是指在编写HTML时,根据页面的内容和结构,选择最能表达该内容本质的标签来标记内容例如对于文章的标签,应该使用<h1>到<h6>标签,而不是使用<span> 或<div> 并通过样式模拟标签样式。代码语义化:代码语义化是指使用具有明确语义的HTML标签,而不是仅仅依赖class和id或style属性来定义元素的外观和行为,使用<nav>标签来表示页面的导航部分,而不是使用<div>并添加class="nav" 3.script标签中defer和async的区别:如果没有defer和async属性,浏览器会立即加载并执行相应的脚本,它不会等待后续加载的文档元素,读取到就会加载和执行,这样就阻塞了后续的文档的加载。无defer和async属性的script标签当浏览器遇到该标签是,会暂停对HTML的解析和渲染,立即开始加载script.js并执行这会阻塞后续文档元素的加载和解析,直到脚本加载和执行完毕defer属性:当使用<script src="script.js" defer></script〉时,脚本的加载是异步的,不会阻塞 HTML 的解析脚本会在 HTML 文档的解析完成后,按照<script>标签在文档中的出现顺序依次执行在 DoMcontentLoaded 事件触发之前。对于多个带有 defer 属性的脚本,它们会按照在文档中的顺序依次执行,即使它们的加载完成顺序不同。<script src="script1.js" defer></script><script src="script2.is" defer></script><script src="script3.is" defer></script>script2.js和script3.js 会并行加载,script1.is 当HTML解析完成后,按照 script1.js、script2.js、script3.js的顺序依次执行DoMcontentLoaded 事件会在所有 defer 脚本执行完成后触发async属性:当使用<script src="script.js"async>/script〉时,脚本的加载也是异步的,不会阻塞 HTML 的解析。一旦脚本加载完成,会立即执行,不管 HTML 解析是否完成。对于多个带有 async 属性的脚本,它们的执行顺序不保证与在文档中的顺序一致,谁先加载完成谁先护行。<script src="script1.is" defer></script><script src="script2.is" defer></script><script src="script3.js" defer></script>script2.is 和 script3.is 会并行加载,script1.is、它们的执行顺序取决于各自的加载完成时间,可能是 script2.js先执行,然后是 script1.js ,最后是 script3.js,顺序不固定。DoMcontentLoaded 事件可能在某些 async 脚本执行之前或之后触发,因为 async 脚本的执行不依赖于HTML 解析完成,也不遵循<script> 标签的顺序。4.常用的meta标签有哪些,作用分别是什么meta 标签由 name 和 content 属性定义,用来描述网页文档的属性,比如网页的作者,网页描述关键词等,除了HTTP标准固定了一些name作为大家使用的共识,开发者还可以自定义name。charset用于指定 HTML 文档的字符编码,确保浏览器能够正确解析文档中的字符。<meta charset="UTF-8">UTF-8 是一种通用的字符编码,支持世界上大多数语言的字符,确保页面可以正确显示中文、英文、特殊字符等,避免出现乱码现象。keywords为搜索引擎提供页面的关键词信息,帮助搜索引擎更好地理解页面的主题和内容,以提高搜索排名<meta name="keywords" content="关键词1, 关键词2, 关键词3">content 属性包含了一系列用逗号分隔的关键词。例如,对于一个关于旅游的页面,可以使用<meta name="keywords"content="旅游,度假,景点,酒店">。搜索引擎会将这些关键词作为页面的主要搜索词,当用户搜索这些关键词时,该页面可能会出现在搜索结果中description提供页面的简短描述,该描述通常会显示在搜索引擎的搜索结果中,作为页面的摘要信息,<meta name="description" content='这是一个关于旅游景点推荐的页面,为你提供各种热门景点的信息和旅游攻略。">content 属性包含了页面的描述信息,长度通常在 150-160 个字符左右。它可以帮助用户快速了解页面的主要内容,吸引用户点击链接。refresh用于页面的自动刷新或重定向。<meta http-equiv="refresh" content="5;url=https://example.com">content 属性中的第一个值表示刷新或重定向的时间间隔(以秒为单位),上述示例中是5 秒。第二个部分( url=https://example.com )表示重定向的目标 URL。例如,<meta http-equiv="refresh" content="g;url=https://newpage.com">会立即将用户重定向至 https://newpage.com。viewport主要用于控制移动端设备的视口,确保页面在移动设备上的显示效果,使其更适应不同屏幕尺寸和分辨率,<meta name="viewport" content="width=device-width, initial-scale=1,maximum-scale=1.0,user-scalable=no">awidth=device-width :将视口的宽度设置为设备的屏幕宽度,确保页面宽度与设备屏幕宽度匹配initial-scale=1初始缩放比例为 1,即页面初始显示不进行缩放。maximum-scale=1.0限制用户可以将页面放大的最大比例为 1.0 倍,防止用户过度放大页面。user-scalable=no禁止用户手动缩放页面,对于某些特定的页面(如应用页面)可能需要此设置,但通常不建议,因为会影响用户体验。———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/2301_81253185/article/details/145422274
-
一、栈1.1. 栈的概念 栈是⼀种特殊的线性表,其只允许在固定的⼀端进⾏插⼊和删除元素操作。进⾏数据插⼊和删除操作 的⼀端称为栈顶,另⼀端称为栈底。栈中的数据元素遵守后进先出的原则。 入栈:栈的插⼊操作叫做进栈/压栈/⼊栈,⼊数据在栈顶。 出栈:栈的删除操作叫做出栈。出数据在栈顶。 1.2. 栈的使用import java.util.Stack; public class Main { public static void main(String[] args) { Stack<Integer> stack = new Stack<>(); }} 我们点进去看一下Stack的源码:public class Stack<E> extends Vector<E> { public Stack() { } public E push(E item) { addElement(item); return item; } 下面是一些Stack的方法,我们来实现一下。 方法 功能Stack() 创建一个空的栈E push(E e) 将e入栈并返回eE pop() 将栈顶元素出栈并返回E peek() 获取栈顶元素int size() 获取栈中有效元素的个数boolean empty() 检查栈是否为空import java.util.Stack; public class Main { public static void main(String[] args) { Stack<Integer> stack = new Stack<>(); System.out.println(stack.empty());//检查栈是否为空 stack.push(5);//入栈 stack.push(4); stack.push(3); stack.push(2); stack.push(1); System.out.println(stack.size());//获取栈的元素个数 System.out.println(stack); System.out.println(stack.empty()); System.out.println(stack.pop());//栈顶元素出栈 System.out.println(stack.peek());//获取栈顶元素 }}1.3. 栈的模拟实现 Stack继承了Vector,Vector和ArrayList类似,都是动态的顺序表,不同的是 Vector是线程安全的,我们可以直接用数组来实现栈。import java.util.Arrays; public class MyStack { private int[] elem; private int usedSize; private static final int DEFAULT = 10; public MyStack(){ elem = new int[DEFAULT]; } public void push(int val){//模拟入栈操作 if(isFull()){//如果满了就扩容 grow(); } elem[usedSize] = val; usedSize++; } private void grow(){ elem = Arrays.copyOf(elem,elem.length); } public boolean isFull(){//判断数组是否满了 return usedSize == elem.length; } public int pop() { if(isEmpty()){ throw new StackIsEmpty("栈为空"); } usedSize--;//相当于把数据置为null return elem[usedSize-1]; } public boolean isEmpty(){ return usedSize == 0; } public int peek() { if(isEmpty()){ throw new StackIsEmpty("栈为空"); } return elem[usedSize--]; } public int size(){ return usedSize; }}public class StackIsEmpty extends RuntimeException{ public StackIsEmpty() { super(); } public StackIsEmpty(String message) { super(message); }}public class Main { public static void main(String[] args) { MyStack stack = new MyStack(); stack.push(1); stack.push(2); stack.push(3); stack.push(4); stack.push(5); System.out.println(stack.size()); int num1 = stack.pop(); System.out.println(num1); int num2 = stack.peek(); System.out.println(num2); }}二、栈的经典面试题2.1. 逆波兰表达式 逆波兰表达式,又称为后缀表达式,特点是运算符位于操作数之后。例如((2+1)*3)、(4+(13/5))是中缀表达式,转化为逆波兰表达式则为2 1 + 3 *、4 13 5 / +。 本题方法参数给出的是字符串数组,我们要先将字符转化为数字。先用引用去遍历数组,如果是数字,就放入栈中;如果引用指向的是运算符,则将栈顶的两个元素出栈进行运算。如此循环往复,直至遍历完整个字符串数组。import java.util.Stack; public class Solution { public int evalRPN(String[] tokens){ Stack<Integer> stack = new Stack<>(); int len = tokens.length; for (int i = 0; i < len; i++) {//遍历字符串数组 String str = tokens[i]; if(!isOperator(str)){ Integer num = Integer.valueOf(str);//将字符转化为数字 stack.push(num); }else{//如果是运算符,则栈顶两个元素出栈 Integer num1 = stack.pop(); Integer num2 = stack.pop(); switch (str){ case "+": stack.push(num2+num1); break; case"-": stack.push(num2-num1); break; case"*": stack.push(num2*num1); break; case"/": stack.push(num2/num1); break; } } } return stack.pop(); } private boolean isOperator(String val){//先判断字符串是数字还是运算符 if(val.equals("+") || val.equals("-") || val.equals("*") || val.equals("/")){ return true; } return false; }}2.2. 有效的括号 题目中要求字符串只有大、中、小括号,返回true的条件为:当我们遍历完字符串之后并且栈为空。如果说,当我们左右括号不匹配时,比如"([))",就返回false;当我们遍历完字符串之后,如果栈不为空,也返回false,比如"(()";当我们还没有遍历完字符串,栈已经为空,也会返回false,比如"({}))"。import java.util.Stack; public class Solution { public boolean isValid(String s){ Stack<Character> stack = new Stack<>(); int len = s.length(); for (int i = 0; i < len; i++) { char ch1 = s.charAt(i); if(ch1 == '{' || ch1 == '[' || ch1 == '('){ stack.push(ch1);//将左括号入栈 }else{ if(stack.isEmpty()){ return false; } char ch2 = stack.peek();//获取入栈的左括号,与右括号进行匹配 if((ch2=='{'&&ch1=='}') || (ch2=='['&&ch1==']') || (ch2=='('&&ch1==')')){ stack.pop(); }else { return false; } } } if(!stack.isEmpty()){ return false; } return true; }}2.3. 最小栈 如果我们抛开这道题,获取栈中的最小元素,我们就可以去遍历这个栈来找出我们的最小元素。但这道题限制我们需要在让时间复杂度为常数,所以说,一个栈是不能解决问题的,还需要在引入一个栈stack。 对于push方法,普通栈当中,所有数据都要放入,最小栈要对我们的普通栈第一次push进行维护。如果最小栈不为空,那么需要比较刚存放的数据与最小栈栈顶的数据进行比较,以对里面的最小值进行更新。这样顺便解决了getMin的实现,直接从最小栈栈顶获取。 对于pop方法,如果弹出的数据不是最小栈的栈顶数据,则只需要弹出普通栈的栈顶数据就行,否则则要弹出最小栈的栈顶数据。top相当于peek方法,只获取普通栈的栈顶元素。 这4个方法基本实现完成,但还有一个问题。如上图所示,如果我们再放入一个-1进入普通栈,那么最小栈需不需要再放入-1呢?答案是需要。因为按照我们的pop方法,把-1弹出,minstack也要弹出。完整代码:import java.util.Stack; public class MinStack { Stack<Integer> stack = new Stack<>(); Stack<Integer> minStack = new Stack<>(); public MinStack() { stack = new Stack<>(); minStack = new Stack<>(); } public void push(int val) { stack.push(val); if(minStack.empty()){ minStack.push(val); }else{ int peekMinVal = minStack.peek(); if(val <= peekMinVal){ minStack.push(val); } } } public void pop() { int val = stack.pop(); if(val == minStack.peek()){ minStack.pop(); } } public int top() { return stack.peek(); } public int getMin() { return minStack.peek(); }———————————————— 原文链接:https://blog.csdn.net/2401_85198927/article/details/145169124
-
引言在当今数字化时代,互联网已然成为人们生活不可或缺的一部分,而网页作为互联网的主要载体,其用户体验的优劣直接关乎着信息的有效传递与用户的留存。JavaScript,这门在前端开发领域占据核心地位的编程语言,犹如一位神奇的魔法师,为静态的网页注入灵动的生命力,使之蜕变成为交互性强、功能丰富的精彩世界。四、事件处理4.1 事件类型在 JavaScript 的前端开发领域,事件处理犹如一座桥梁,紧密连接着用户与网页之间的交互。它能够精准捕捉用户在页面上的各类操作,诸如鼠标的轻轻点击、键盘的敲击输入、表单的提交确认等,并迅速触发相应的 JavaScript 代码来执行特定功能,为用户带来流畅且自然的交互体验。鼠标事件堪称交互中的 “主力军”,涵盖了诸多常见类型。click 事件,作为最为常用的一种,当用户在某个元素上执行单击鼠标左键的操作时,便会如同触动了机关一般被触发,就像在网页上点击一个按钮提交表单,或是打开一个链接跳转页面。与之紧密相关的 dblclick 事件,则要求用户在极短时间内连续双击鼠标左键,常用于实现一些快速操作指令,比如在图片编辑场景下,双击图片快速进入编辑模式。mouseover 与 mouseenter 事件均在鼠标指针移至元素上方时触发,细微差别在于,mouseover 在鼠标经过元素及其子元素时都会触发,而 mouseenter 仅当鼠标初次进入元素自身范围时才触发,二者适用于不同的交互细节需求,如导航菜单的展开,mouseover 能让子菜单在鼠标滑过主菜单及子项时都灵活响应,mouseenter 则可确保仅在精准指向主菜单时才触发展开动作,避免误触。mouseout 与 mouseleave 事件则相反,对应鼠标离开元素的操作,其触发规则与上述类似,常用于收起菜单、隐藏提示信息等场景。mousedown 与 mouseup 事件分别对应鼠标按钮按下与松开的瞬间,这两个事件常与 mousemove 配合,用于实现诸如拖拽元素、绘制图形等复杂交互,像在一些图形设计软件的网页版中,用户按下鼠标并移动来绘制线条,松开鼠标完成绘制。键盘事件同样不可或缺,keydown 事件在用户按下键盘上任意键的瞬间被触发,无论是字母、数字、符号还是功能键,它都能敏锐捕捉,通过监听此事件,开发者可实时获取用户的按键输入,常用于文本输入实时校验、快捷键响应等场景。例如在一些在线文档编辑页面,输入文字时实时检查拼写错误,或是按下 Ctrl + S 组合键触发保存操作。keyup 事件紧随 keydown 之后,在按键松开时触发,与 keydown 配合可精准判断用户完整的按键动作,确保交互逻辑的准确性。需注意的是,曾经的 keypress 事件在按下字符键时触发,但已逐渐被 keydown 取代,因其在处理功能键等方面存在局限。表单事件聚焦于表单元素的交互处理。submit 事件在用户点击表单的提交按钮,或是在表单内按下回车键(前提是表单设置允许)时触发,此时通常会进行表单数据的校验与提交操作,如登录表单验证用户名和密码是否符合格式要求、是否非空等,若校验通过则向服务器发送数据。change 事件则针对表单元素状态的改变,当单选框、复选框被选中或取消选中,下拉列表选择了不同选项,文本框或 textarea 元素内容改变且失去焦点时,都会触发该事件,常用于实时更新相关联的显示内容或执行额外校验,比如电商购物选择商品规格后,实时更新商品总价;在文本框输入完信息,失去焦点时检查格式是否正确。input 事件与 change 类似,不过它更加 “实时”,只要表单元素的值发生变化,便会立即触发,对于实时反馈用户输入极为有用,如搜索框实时显示输入的关键词联想结果。不妨以一个简单实例来深入理解事件驱动机制。在网页上有一个按钮,当用户点击它时,弹出一个提示框显示 “按钮被点击了”:<button id="myButton">点击我</button><script> document.getElementById('myButton').addEventListener('click', function() { alert('按钮被点击了'); });</script>在此例中,按钮是事件源,click 是事件类型,而函数 function() { alert('按钮被点击了'); } 便是事件处理程序。当用户执行点击操作这一 “导火索” 时,便迅速激活事件处理程序,进而弹出提示框,完成一次流畅的交互。4.2 事件监听器在 JavaScript 前端开发中,事件监听器是实现高效、灵活事件处理的关键所在。它宛如一位忠诚且机智的 “守护者”,时刻监听着特定 DOM 元素上的各种事件,一旦捕捉到目标事件的发生,便会立即触发与之绑定的相应函数,执行预设的交互逻辑。addEventListener 方法无疑是其中的 “核心利器”,它以一种极为灵活且强大的方式,将事件与处理函数紧密关联起来。其语法结构如下:target.addEventListener(type, listener, useCapture);其中,target 代表目标 DOM 元素,通过诸如 document.getElementById、querySelector 等方法精准获取;type 即为要监听的事件类型,如前文提及的 click、keydown、submit 等;listener 则是对应的事件处理函数,它可以是具名函数,也可以是匿名函数,这个函数承载着当事件触发时需要执行的具体代码逻辑;useCapture 是一个可选参数,用于指定事件冒泡或捕获阶段,默认为 false,即处于冒泡阶段,后续会详细讲解冒泡与捕获机制。以一个为按钮添加点击监听器的实例来深入剖析:<button id="actionButton">执行操作</button><script> const button = document.getElementById('actionButton'); button.addEventListener('click', function() { console.log('按钮被点击,即将执行重要操作...'); // 此处可添加具体业务逻辑代码,如发送AJAX请求、更新页面数据等 });</script>在上述代码中,首先通过 document.getElementById 精准定位到 id 为 'actionButton' 的按钮元素,将其赋值给 button 变量。接着,使用 addEventListener 方法为该按钮监听 'click' 事件,当用户点击按钮时,匿名函数内的代码便会立即执行,控制台输出相应提示信息,并且可依需求在此处拓展更为复杂的业务逻辑,如向服务器提交表单数据、动态更新页面 UI 等。与传统的内联事件处理属性相比,addEventListener 具有显著优势。传统的内联方式,如在 HTML 标签内直接使用 onclick="function ()",虽然简单直接,但却将 JavaScript 代码与 HTML 结构紧密耦合在一起,使得代码的维护性与可扩展性大打折扣。一旦业务逻辑复杂起来,需要修改或新增功能,在 HTML 标签内嵌入的大量代码将变得混乱不堪,难以管理。而 addEventListener 将事件绑定逻辑统一放置在 JavaScript 脚本中,实现了 HTML 结构与 JavaScript 行为的分离,遵循了良好的代码解耦原则,使得代码结构更加清晰、易于维护与拓展。在实际应用场景中,比如一个网页表单包含多个按钮,分别用于提交表单、重置表单、保存草稿等不同操作,便可利用 addEventListener 为每个按钮绑定各自专属的点击处理函数:<form id="myForm"> <input type="text" placeholder="请输入内容"> <button id="submitButton">提交</button> <button id="resetButton">重置</button> <button id="saveButton">保存草稿</button></form><script> const submitBtn = document.getElementById('submitButton'); const resetBtn = document.getElementById('resetButton'); const saveBtn = document.getElementById('saveButton'); submitBtn.addEventListener('click', function() { // 执行表单提交逻辑,如校验数据、发送请求 console.log('表单提交中...'); }); resetBtn.addEventListener('click', function() { // 重置表单数据,恢复初始状态 console.log('表单已重置'); }); saveBtn.addEventListener('click', function() { // 保存草稿逻辑,如将数据暂存本地 console.log('草稿已保存'); });</script>如此一来,各个按钮各司其职,通过独立的事件监听器实现了多样化的交互响应,极大提升了用户体验与代码的灵活性。五、实战案例:打造简易待办事项列表5.1 HTML 结构搭建在着手构建简易待办事项列表时,精心搭建 HTML 结构是基础且关键的第一步。它犹如搭建房屋的骨架,为后续功能的添砖加瓦提供稳固支撑。以下是一段基础的 HTML 代码示例:<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>简易待办事项列表</title> <link rel="stylesheet" href="styles.css"></head> <body> <h1>待办事项列表</h1> <div id="todo-app"> <input type="text" id="new-todo" placeholder="添加新任务..."> <button id="add-todo">添加</button> <ul id="todo-list"> </ul> </div> <script src="script.js"></script></body> </html>在这段代码中,<h1> 标签醒目地展示了应用的标题,让用户一眼便能知晓此页面的用途。<div id="todo-app"> 作为核心容器,将整个待办事项的操作区域进行了整合,使之在页面布局上更为规整。其中,<input type="text" id="new-todo"> 是用户输入待办任务的入口,placeholder 属性友好地提示用户应在此处输入内容;<button id="add-todo"> 则是触发添加任务动作的按钮,简洁直观。而 <ul id="todo-list"> 如同一个空白的任务收纳盒,后续通过 JavaScript 动态生成的任务列表项(<li> 元素)都将被有序地放置其中,为用户呈现清晰的任务清单。合理且简洁的 HTML 结构设计,不仅提升了代码的可读性,更为后续 JavaScript 功能的实现铺就了顺畅之路。5.2 JavaScript 功能实现有了 HTML 结构作为基石,接下来借助 JavaScript 赋予待办事项列表鲜活的交互能力。以下是实现添加任务、删除任务以及标记任务完成功能的 JavaScript 代码示例,并附有详细注释:// 等待页面DOM加载完成后再执行后续代码,确保DOM元素已存在document.addEventListener('DOMContentLoaded', function () { const todoInput = document.getElementById('new-todo'); const addButton = document.getElementById('add-todo'); const todoList = document.getElementById('todo-list'); // 为添加按钮添加点击事件监听器 addButton.addEventListener('click', function () { const taskText = todoInput.value.trim(); if (taskText!== '') { // 创建新的列表项元素 const listItem = document.createElement('li'); listItem.textContent = taskText; // 创建删除按钮 const deleteButton = document.createElement('button'); deleteButton.textContent = '删除'; // 为删除按钮添加点击事件监听器,点击时移除对应的列表项 deleteButton.addEventListener('click', function () { todoList.removeChild(listItem); }); // 将删除按钮添加到列表项中 listItem.appendChild(deleteButton); // 将新的列表项添加到待办事项列表 todoList.appendChild(listItem); // 清空输入框,以便用户输入下一个任务 todoInput.value = ''; } }); // 为待办事项列表添加点击事件监听器,用于标记任务完成 todoList.addEventListener('click', function (e) { if (e.target.tagName === 'LI') { e.target.classList.toggle('completed'); } });});在上述代码中,首先使用 document.addEventListener('DOMContentLoaded', function () {...}) 确保整个 HTML 页面的 DOM 结构加载完毕后,才开始执行后续的 JavaScript 代码。这一步骤至关重要,因为若过早执行,可能会因 DOM 元素未完全加载而导致无法获取到相应元素,引发错误。接着,通过 document.getElementById 精准获取到输入框、添加按钮以及待办事项列表的 DOM 元素引用,并分别存储在 todoInput、addButton 和 todoList 变量中,方便后续操作。当用户在输入框输入任务文本并点击添加按钮时,addButton.addEventListener('click', function () {...}) 中的代码被触发。首先,获取输入框中的文本并去除首尾空格,若文本不为空,则开启创建新任务列表项的流程。使用 document.createElement('li') 生成一个新的 <li> 元素,并将输入的任务文本赋值给它的 textContent 属性,使其显示在页面上。同时,创建一个用于删除任务的按钮,同样为其添加点击事件监听器,当点击删除按钮时,执行 todoList.removeChild(listItem),直接从 DOM 树中移除对应的列表项,实现任务删除功能。最后,将新创建的列表项添加到待办事项列表中,并清空输入框,等待用户输入下一个任务。为了实现标记任务完成的功能,利用 todoList.addEventListener('click', function (e) {...}) 为整个待办事项列表添加点击事件监听器。当用户点击列表中的某个任务项(<li> 元素)时,通过判断 e.target.tagName === 'LI',确认点击的是任务项本身,随后使用 e.target.classList.toggle('completed'),动态切换任务项的 completed 类名。在 CSS 样式表中,可预先定义 .completed 类的样式,如添加删除线、改变字体颜色等,以此直观地呈现任务的完成状态,为用户提供清晰的视觉反馈,让待办事项管理更加便捷高效。六、进阶拓展:异步编程与 Ajax6.1 异步编程概念在 JavaScript 的编程世界里,同步与异步犹如两条截然不同的执行路径,深刻影响着程序的运行逻辑与用户体验。同步编程,恰似一位按部就班的 “执行者”,每一行代码都必须严格遵循顺序依次执行,犹如工厂流水线上的一道道工序,前一个任务未完成,后续任务只能默默等待。以读取本地文件为例,若使用同步方式,程序会在发出读取指令后,如同被定格一般,死死 “卡住”,直至文件完整读取并返回结果,才肯继续执行下一行代码。这种 “死等” 模式,在处理耗时较短的任务时,或许尚可接受;但一旦遭遇如大规模数据读取、复杂网络请求等耗时漫长的操作,问题便会接踵而至。整个程序仿佛陷入泥沼,界面冻结,用户的任何操作都得不到即时响应,极大地损害了用户体验。而异步编程,则像是一位高效的 “多面手”,当遇到诸如网络请求、文件读取这类耗时操作时,它不会傻傻等待,而是迅速开启新的任务分支,将后续代码的执行权交予主线程,让程序得以继续流畅运行。以网页加载图片为例,当浏览器发起图片加载请求后,并不会停滞不前,而是立即着手处理其他页面元素的渲染、脚本的执行等任务。待图片数据从服务器慢悠悠地传输回来,再由专门的回调函数或异步处理机制,将图片巧妙地安置到对应的位置。如此一来,用户便能在图片加载的间隙,正常进行页面滚动、点击链接等操作,页面始终保持着鲜活的响应能力,极大提升了交互的流畅性。再看一个从服务器获取数据来更新页面内容的场景。在同步模式下,页面会在数据请求发出后陷入僵局,直到数据完整抵达,才一次性更新页面,这期间用户面对的是毫无变化的 “白板”,极易产生焦虑与不耐烦。而采用异步编程,数据请求悄然在后台运作,主线程继续渲染页面骨架、设置基础样式,待数据到手,再通过 DOM 操作逐步、动态地填充内容,用户看到的是一个逐步鲜活起来的页面,交互体验天差地别。异步编程的核心优势,就在于巧妙地利用等待时间,让程序的各个部分并行推进,避免因个别耗时操作拖垮整个系统的响应速度,为用户带来丝滑流畅的浏览与操作体验。6.2 Ajax 原理与使用Ajax(Asynchronous JavaScript and XML),作为前端开发领域的一项关键技术,犹如一座隐形的桥梁,无缝连接着前端页面与后端服务器,实现了数据的异步交互与页面的局部更新,为用户带来流畅、高效的浏览体验。其核心原理依托于 XMLHttpRequest 对象(在现代浏览器中,也常使用更为简洁的 Fetch API),这一对象恰似一位 “幕后信使”,能够在不刷新整个页面的前提下,悄然向服务器发送 HTTP 请求,并机智地接收、处理服务器返回的数据。以一个常见的网页场景为例,当用户在搜索框输入关键词,期望实时获取搜索建议时,Ajax 便开始大展身手。使用 XMLHttpRequest 对象发起请求,代码大致如下:const xhr = new XMLHttpRequest();xhr.open('GET', 'https://example.com/api/search?q=' + encodeURIComponent(keyword), true);xhr.onreadystatechange = function() { if (xhr.readyState === 4 && xhr.status === 200) { const response = JSON.parse(xhr.responseText); // 处理搜索建议数据,如更新下拉列表展示 }};xhr.send();这里,首先创建了 XMLHttpRequest 实例,通过 open 方法精心配置请求的类型(GET)、目标 URL(包含搜索关键词)以及异步模式(true)。接着,为 onreadystatechange 事件绑定回调函数,宛如设置了一个敏锐的 “瞭望哨”,时刻紧盯请求状态的变化。当 readyState 达到 4(意味着请求已完成,数据接收完毕)且状态码为 200(表示请求成功)时,便迅速对返回的 JSON 数据进行解析,并依据数据内容更新页面的搜索建议区域,整个过程页面纹丝不动,用户却能实时获取反馈。Fetch API 则以一种更加现代化、简洁的语法实现类似功能:fetch('https://example.com/api/search?q=' + encodeURIComponent(keyword)) .then(response => response.json()) .then(data => { // 处理搜索建议数据 }) .catch(error => { console.error('搜索请求出错:', error); });Fetch API 采用链式调用的 Promise 风格,通过 then 方法依次处理请求成功后的响应解析、数据处理步骤,若途中出现错误,catch 方法便能精准捕获并处理,让异步数据交互的代码逻辑更加清晰、易读。不妨再看一个完整的示例,从服务器获取待办事项列表数据并实时更新页面展示:<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Ajax待办事项示例</title> <style> #todo-list { list-style-type: none; padding: 0; } #todo-list li { border: 1px solid #ccc; margin: 5px; padding: 5px; } </style></head> <body> <h1>待办事项列表</h1> <ul id="todo-list"></ul> <script> document.addEventListener('DOMContentLoaded', function () { const todoList = document.getElementById('todo-list'); // 使用Fetch API获取待办事项数据 fetch('https://example.com/api/todos') .then(response => response.json()) .then(todos => { todos.forEach(todo => { const listItem = document.createElement('li'); listItem.textContent = todo.task; todoList.appendChild(listItem); }); }) .catch(error => { console.error('获取待办事项出错:', error); }); }); </script></body> </html>在上述代码中,页面加载完成后,通过 Fetch API 向指定服务器接口发送请求,成功获取数据后,循环遍历待办事项数组,动态创建 DOM 元素并添加到页面列表中,瞬间为用户呈现出最新的待办任务清单,全程无刷新,交互体验流畅自然。Ajax 技术的应用,让网页告别了频繁整页刷新的笨拙,实现了数据与页面展示的精妙同步,极大提升了用户体验与应用的响应效率。七、前沿框架:Vue.js 入门窥探7.1 Vue.js 简介在当今蓬勃发展的前端开发领域,Vue.js 宛如一颗璀璨夺目的新星,以其卓越的特性迅速赢得了广大开发者的青睐,成为构建现代用户界面的得力工具。Vue.js 最为突出的优势之一便是其精妙绝伦的响应式数据绑定机制。传统的 JavaScript 开发模式下,当数据发生变化时,开发者需手动编写冗长繁杂的代码来精准定位并更新对应的 DOM 元素,这一过程极易出错且效率低下,犹如在错综复杂的迷宫中艰难寻路。而 Vue.js 通过其内部强大的响应式系统,能够自动 “感知” 数据的细微变化,宛如一位时刻警觉的守护者,一旦数据有所异动,便立即高效且智能地更新与之关联的 DOM 内容,确保视图与数据始终保持高度一致,实现无缝同步。组件化开发则是 Vue.js 的另一大 “杀手锏”。它倡导将复杂的用户界面拆解为一个个独立、可复用的小型组件,恰似将一座宏伟的大厦拆分为众多标准化的积木模块。每个组件都拥有自己独立的 HTML 模板、JavaScript 逻辑以及 CSS 样式,它们既能在不同场景下被重复调用,又能依据需求灵活组合,极大地提升了开发效率与代码的可维护性。例如,在构建一个大型电商网站时,头部导航栏、商品列表、购物车等功能模块均可封装为独立组件,开发团队可并行推进各组件的开发,后续若需优化某个组件,也只需聚焦于该组件内部代码,避免牵一发而动全身,让项目开发与维护变得井井有条。对比原生 JavaScript 开发,Vue.js 的高效性体现得淋漓尽致。以构建一个具有动态数据展示与交互功能的页面为例,原生 JavaScript 需要耗费大量精力处理 DOM 操作、事件绑定以及数据更新的复杂逻辑,代码往往冗长且晦涩难懂,如同杂乱无章的线缆交织在一起;而 Vue.js 凭借简洁优雅的模板语法、高效的数据绑定以及组件化架构,能够以更少的代码量、更清晰的逻辑结构实现同样甚至更为强大的功能,宛如一位技艺精湛的魔法师,用简洁的咒语变出绚丽的魔法,让开发者从繁琐的底层操作中解脱出来,将更多精力投入到业务逻辑与用户体验的优化上,快速打造出高性能、交互性强的优质网页应用。7.2 基础使用示例下面通过一个简单的计数器示例,来初步领略 Vue.js 的魅力与便捷。首先,引入 Vue.js 库。可以通过在 HTML 页面的<head>标签内使用<script>标签引入 CDN 链接,如下:<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>接着,在 HTML 的<body>标签内创建一个 DOM 元素作为 Vue 实例的挂载点,例如:<div id="app"></div>然后,编写 JavaScript 代码创建 Vue 实例并进行数据绑定:var app = new Vue({ el: '#app', data: { count: 0 }});这里,el属性指定了 Vue 实例挂载的 DOM 元素选择器,data对象则包含了应用所需的数据,此处仅有一个count属性,初始值为 0。在 HTML 中,使用 Vue.js 的模板语法将数据绑定到 DOM 元素上:<div id="app"> <p>当前计数:{ { count }}</p> <button @click="count++">点击增加</button></div>在上述代码中,双大括号{ { count }}便是 Vue.js 的文本插值语法,它能够实时将count的数据渲染到 DOM 中,让用户直观看到当前计数。而@click="count++"则是 Vue.js 的事件绑定语法,它监听按钮的点击事件,每次点击时,count的值便会自动加 1,由于 Vue.js 的响应式特性,与之绑定的 DOM 内容也会瞬间更新,完美展现数据双向绑定的效果。不妨设想一下,如果使用原生 JavaScript 来实现相同功能,需要手动获取 DOM 元素、监听按钮点击事件、更新数据并操作 DOM 来修改显示文本,代码复杂度大幅提升,且易出现诸如事件绑定错误、DOM 更新不及时等问题。而 Vue.js 通过简洁的语法糖,将复杂的交互逻辑封装得优雅而高效,让开发者能轻松构建动态交互界面,开启便捷开发之旅。八、总结与展望至此,我们已一同穿越了 JavaScript 前端开发的核心地带,领略了其从基础语法、DOM 操作、事件处理,到实战应用、异步编程以及前沿框架 Vue.js 入门的独特魅力。在这个过程中,我们明晰了变量与数据类型的精妙差异,熟练掌握了条件循环语句与函数的灵活运用,学会了运用 DOM 操作精准掌控网页元素,巧用事件处理搭建起用户与网页交互的坚实桥梁,通过实战打造出实用的待办事项列表,深入理解异步编程提升页面性能,并初探 Vue.js 感受现代框架的高效便捷。然而,JavaScript 的世界广袤无垠,始终处于蓬勃发展之中。新的特性、框架、工具如繁星般不断涌现。作为前端开发者,持续学习是我们前行的不二法则。需时刻关注 ECMAScript 的最新标准,探索如 React、Angular 等其他前沿框架的独特优势,深入钻研 WebAssembly、PWA 等新兴技术,将其巧妙融入项目,创造更为精彩卓越的用户交互体验。愿大家在 JavaScript 前端开发的征程中,不断探索、砥砺奋进,书写属于自己的精彩篇章,让互联网世界因我们的代码而绽放更加绚烂的光彩。———————————————— 原文链接:https://blog.csdn.net/weixin_73295475/article/details/145325390
-
引言在多线程编程中,线程间的数据共享与隔离是一个非常重要的话题。Java 提供了多种机制来处理多线程环境下的数据共享问题,其中 ThreadLocal 是一个非常有用的工具。ThreadLocal 允许我们为每个线程创建一个独立的变量副本,从而避免线程间的数据竞争和同步问题。本文将深入探讨 ThreadLocal 的工作原理,并通过代码示例展示如何在 Java 多线程环境中使用 ThreadLocal 进行上下文管理。一、ThreadLocal 的基本概念1.1 什么是 ThreadLocal?ThreadLocal 是 Java 提供的一个线程级别的变量存储类。它为每个使用该变量的线程提供了一个独立的变量副本,每个线程都可以独立地改变自己的副本,而不会影响其他线程的副本。ThreadLocal 通常用于在多线程环境中保存线程的上下文信息,如用户会话、数据库连接等。1.2 ThreadLocal 的使用场景线程上下文管理:在多线程环境中,ThreadLocal 可以用于保存线程的上下文信息,如用户会话、事务 ID 等。避免参数传递:在某些情况下,ThreadLocal 可以避免在方法调用链中传递参数,简化代码。线程安全的对象管理:ThreadLocal 可以用于管理线程安全的对象,如 SimpleDateFormat 等。二、ThreadLocal 的工作原理2.1 ThreadLocal 的内部结构ThreadLocal 的核心思想是为每个线程维护一个独立的变量副本。为了实现这一点,ThreadLocal 内部使用了一个名为 ThreadLocalMap 的静态内部类。ThreadLocalMap 是一个定制化的哈希表,用于存储线程的变量副本。每个 Thread 对象内部都有一个 ThreadLocalMap 的实例,用于存储该线程的所有 ThreadLocal 变量。当调用 ThreadLocal 的 get() 或 set() 方法时,ThreadLocal 会首先获取当前线程的 ThreadLocalMap,然后在该 ThreadLocalMap 中进行操作。2.2 ThreadLocal 的核心方法set(T value):将当前线程的 ThreadLocal 变量副本设置为指定的值。get():返回当前线程的 ThreadLocal 变量副本。remove():移除当前线程的 ThreadLocal 变量副本。2.3 ThreadLocalMap 的实现ThreadLocalMap 是一个定制化的哈希表,它的键是 ThreadLocal 对象,值是对应的变量副本。ThreadLocalMap 使用开放地址法来解决哈希冲突,即当发生冲突时,它会寻找下一个空闲的槽位来存储数据。ThreadLocalMap 的键是弱引用(WeakReference),这意味着当 ThreadLocal 对象不再被引用时,它会被垃圾回收器回收,从而避免内存泄漏。三、ThreadLocal 的使用示例3.1 基本使用下面是一个简单的示例,展示了如何使用 ThreadLocal 来保存线程的上下文信息。public class ThreadLocalExample { private static final ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { Runnable task = () -> { // 设置线程的上下文信息 threadLocal.set(Thread.currentThread().getName() + " - context"); // 获取线程的上下文信息 System.out.println(threadLocal.get()); // 清除线程的上下文信息 threadLocal.remove(); }; // 创建多个线程并启动 Thread thread1 = new Thread(task); Thread thread2 = new Thread(task); thread1.start(); thread2.start(); }}在这个示例中,我们创建了一个 ThreadLocal 变量 threadLocal,并在每个线程中设置和获取该变量的值。由于 ThreadLocal 为每个线程提供了独立的变量副本,因此每个线程都可以安全地访问和修改自己的副本,而不会影响其他线程。3.2 使用 ThreadLocal 管理线程安全的对象ThreadLocal 还可以用于管理线程安全的对象,如 SimpleDateFormat。由于 SimpleDateFormat 不是线程安全的,因此在多线程环境中使用时需要进行同步。通过 ThreadLocal,我们可以为每个线程创建一个独立的 SimpleDateFormat 实例,从而避免同步开销。public class ThreadLocalDateFormat { private static final ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); public static String formatDate(Date date) { return dateFormatThreadLocal.get().format(date); } public static void main(String[] args) { Runnable task = () -> { String formattedDate = formatDate(new Date()); System.out.println(Thread.currentThread().getName() + " - " + formattedDate); }; // 创建多个线程并启动 Thread thread1 = new Thread(task); Thread thread2 = new Thread(task); thread1.start(); thread2.start(); }}在这个示例中,我们使用 ThreadLocal 为每个线程创建了一个独立的 SimpleDateFormat 实例。这样,每个线程都可以安全地使用自己的 SimpleDateFormat 实例,而无需担心线程安全问题。四、ThreadLocal 的内存泄漏问题4.1 内存泄漏的原因虽然 ThreadLocal 提供了线程级别的变量隔离,但如果使用不当,可能会导致内存泄漏问题。ThreadLocalMap 中的键是弱引用,这意味着当 ThreadLocal 对象不再被引用时,它会被垃圾回收器回收。然而,ThreadLocalMap 中的值仍然是强引用,因此如果 ThreadLocal 对象被回收,但 ThreadLocalMap 中的值没有被清除,就会导致内存泄漏。4.2 如何避免内存泄漏为了避免内存泄漏,我们应该在使用完 ThreadLocal 变量后,调用 remove() 方法将其从 ThreadLocalMap 中移除。这样可以确保 ThreadLocalMap 中的值不会一直保留,从而避免内存泄漏。public class ThreadLocalMemoryLeakExample { private static final ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { Runnable task = () -> { try { // 设置线程的上下文信息 threadLocal.set(Thread.currentThread().getName() + " - context"); // 模拟业务逻辑 System.out.println(threadLocal.get()); } finally { // 清除线程的上下文信息 threadLocal.remove(); } }; // 创建多个线程并启动 Thread thread1 = new Thread(task); Thread thread2 = new Thread(task); thread1.start(); thread2.start(); }}在这个示例中,我们在 finally 块中调用了 threadLocal.remove() 方法,以确保在使用完 ThreadLocal 变量后将其清除,从而避免内存泄漏。五、总结ThreadLocal 是 Java 多线程编程中一个非常有用的工具,它允许我们为每个线程创建一个独立的变量副本,从而避免线程间的数据竞争和同步问题。通过 ThreadLocal,我们可以轻松地管理线程的上下文信息,简化代码,并提高程序的性能。然而,ThreadLocal 也存在内存泄漏的风险,因此在使用时需要注意及时清理不再需要的变量副本。通过合理地使用 ThreadLocal,我们可以在多线程环境中更好地管理线程的上下文信息,提高程序的稳定性和可维护性。希望本文能够帮助你更好地理解 ThreadLocal 的工作原理,并在实际开发中灵活运用它来解决多线程环境下的数据共享与隔离问题————————————————原文链接:https://blog.csdn.net/weixin_44976692/article/details/145410897
-
枚举是什么枚举(enum):是一种特殊的类,用于定义一组常量,将其组织起来。枚举使得代码更具有可读性和可维护性,特别是在处理固定集合的值时,如:星期、月份、状态码等在 Java 中,使用关键字 enum 来定义枚举类:public enum Day { SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY;}其中,定义的枚举项就是该类的实例,且必须在第一行,最后一个枚举项后的分号; 可以省略,但是若枚举类有其他内容,则分号不能省略(最好不要省略) 当类初始化时,这些枚举项就会被实例化枚举类使用 enum 定义后,默认继承 java.lang.Enum 类,也就是说,我们自己写的枚举类,就算没有显示的继承 Enum,但是其默认继承了这个类此外,枚举在 Java 中不能被继承,自定义的枚举类隐式继承自 java.lang.Enum 类,且不能再继承其他类,这样的设计确保了枚举类的简单性和一致性。如果枚举可以继承其他类,将会导致复杂的继承关系,并且影响Java的类型系统常用方法方法 描述values() 以数组的形式返回枚举类型的所有成员ordinal()获取枚举成员的索引位置valueOf() 将普通字符串转换为枚举实例compareTo(E o)比较两个枚举成员在定义时的顺序 我们通过一个示例,来学习和使用这些方法:public enum Day { SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY; public static void main(String[] args) { // 获取所有枚举成员 Day[] days = Day.values(); // 遍历 for (int i = 0; i < days.length; i++) { // 获取枚举成员以及索引位置 System.out.println(days[i] + " " + days[i].ordinal()); } // 将普通字符串转换为枚举实例 System.out.println(Day.valueOf("THURSDAY")); // 获取枚举实例 SUNDAY 和 SATURDAY Day sunday = Day.SUNDAY; Day saturday = Day.SATURDAY; // 比较定义时的顺序 System.out.println(sunday.compareTo(saturday)); }}运行结果: 在使用 valueOf() 方法进行转换时,传递的名称必须与枚举常量的名字完全匹配(包括大小写),若不匹配,就会抛出 IllegalArgumentException 异常: 当我们查看 java.lang.Enum 时: 可以看到,valueOf() 方法包含了两个类型的参数:Class<T> enumClass 和 String name其中Class<T> enumType 是一个 Class 对象,表示要查找的枚举类型String name: 是一个字符串,表示要查找的枚举常量的名称。名称必须与枚举常量的名字完全匹配(包括大小写) 但是在使用时,我们只传递了一个参数 name,也能够进行转换,这是为什么呢?这是因为,在 Java 中,valueOf 方法实际上是自动生成的,属于每个枚举类型的特性。虽然它的原始定义需要两个参数(类类型和名称),但是每个枚举类型都会自动提供一个与自身类型相关联的 valueOf 方法,只需传递一个字符串参数因此,当我们调用 Day.valueOf("THURSDAY") 时,Java会自动处理这个调用,实际调用的是包含类名的 valueOf 方法,而不是原始的静态方法定义再观察 java.lang.Enum: 我们会发现,其中并不存在 values() 这个方法,而当我们点击 values() 方法时,则会跳转到本类上那么,values() 方法是从哪来的呢?values() 方法是枚举类自动提供的一个静态方法,允许我们获取一个包含所有枚举常量的数组,这个方法是由Java编译器自动生成的,在编译时每个枚举类型都会自动生成一个 values() 方法,因此不需要我们显式定义它我们将枚举类进行反编译:(1)打开 cmd,切换到 Day.java 文件所在目录(2)编译 .java 文件(javac Day.java)(3)将 .class 文件进行反编译(javap -c Day.class > day.txt)打开 day.txt,可以看到: 编译器自动为我们生成了 values 和 valueOf 方法构造方法当我们创建构造方法时: 不能使用 public 来修饰构造方法,为什么呢?这是因为,在 Java 中,枚举类的构造方法都是私有的(不加任何修饰符时,默认是 private),无法在枚举类外部调用,这也就防止了在枚举类外部创建新的枚举常量在定义枚举常量时,构造方法会被隐式调用:public enum Day { SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY; Day() { System.out.println("构造方法"); } public static void main(String[] args) { System.out.println("------------------"); }}运行结果: 当我们定义带有参数的构造方法时,在创建枚举项时,也要为其提供对应参数:public enum Day { SUNDAY("周天", 7), MONDAY("周一", 1), TUESDAY("周二", 2), WEDNESDAY("周三", 3), THURSDAY("周四", 4), FRIDAY("周五", 5), SATURDAY("周六", 6); private String name; private int key; Day(String name, int key) { this.name = name; this.key = key; }}枚举的优缺点优点:(1)枚举常量确保了只能使用定义的常量,避免了使用整型常量时可能带来的错误(2)使用枚举可以使代码更易读,表达清晰(3)枚举定义了一组固定的常量,适合表示有限的状态或选项,便于管理和维护(4)枚举可以拥有字段、方法和构造方法,能够封装与常量相关的行为和属性(5)Java的枚举类自带一些方法,如 values()、valueOf() 等(6)可以用于 switch 语句缺点:(1)枚举的集合是固定的,无法在运行时添加或删除常量。如果需要动态的集合,枚举可能不适用(2)枚举不能继承,无法扩展(3)每个枚举常量都是一个对象,可能会增加内存使用,特别是当枚举常量数量较多时枚举和反射在 Java 反射-CSDN博客 中,我们学习了反射,通过反射,我们可以拿到类的私有构造方法,从而创建实例对象那么,枚举是否可以通过反射,拿到实例对象呢?public enum Day { SUNDAY("周天", 7), MONDAY("周一", 1), TUESDAY("周二", 2), WEDNESDAY("周三", 3), THURSDAY("周四", 4), FRIDAY("周五", 5), SATURDAY("周六", 6); private String name; private int key; Day(String name, int key) { this.name = name; this.key = key; }}public class Test { public static void main(String[] args) { Class<?> classDay = null; try { classDay = Class.forName("Day"); Constructor<?> constructor = classDay.getDeclaredConstructor(String.class, int.class); constructor.setAccessible(true); Day day = (Day) constructor.newInstance("sunday", 0); System.out.println(day.ordinal()); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } catch (InstantiationException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } }}运行结果: 此时程序抛出了 NoSuchMethodException 异常,也就是没有对应的构造方法但是提供的枚举的构造方法就是带有两个参数,分别为 String 和 int: 那么,问题出在哪里呢?自定义的枚举类默认继承自 java.lang.Enum因此,自定义的枚举类继承了父类除构造方法外的所有东西,且子类需要帮助父类进行构造,但我们实现的类中,并没有帮助父类进行构造因此,我们需要在枚举类中帮助父类进行构造,而父类中的构造方法为: 那么,如何实现呢?通过 super 方法吗?但是,当我们在构造方法中调用 super 时: 枚举构造方法中不能使用 super 由于枚举比较特殊,在构造方法中,除了我们自定义了两个参数,它还默认添加了父类的两个参数也就是说,构造函数中一共有四个参数:String int String int其中,前两个参数是父类参数,后两个参数是子类参数public class Test { public static void main(String[] args) { Class<?> classDay = null; try { classDay = Class.forName("enumDemo.Day"); Constructor<?> constructor = classDay.getDeclaredConstructor(String.class, int.class, String.class, int.class); constructor.setAccessible(true); Day day = (Day) constructor.newInstance("父类参数", 0, "子类参数", 0); System.out.println(day.ordinal()); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } catch (InstantiationException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } }}再次运行: 此时抛出了 IllegalArgumentException 异常,不能通过反射创建枚举对象枚举保证了每个枚举常量只有一个实例,这种唯一性在枚举类型被定义时就已经确定,不允许外部创建新的实例,枚举类型的设计使得它们的实例在类加载时被唯一地定义,从而避免了通过反射创建新的枚举实例的可能性,确保了枚举的强类型安全性和唯一性实现单例模式在 单例模式:饿汉模式、懒汉模式_单例模式懒汉和饿汉-CSDN博客 中我们实现了单例模式,单例模式能够确保一个类只有一个实例但普通类可以通过反射机制打破,因此,我们可以使用枚举来实现单例模式public enum Singleton { INSTANCE; public Singleton getInstance() { return INSTANCE; }———————————————— 原文链接:https://blog.csdn.net/2301_76161469/article/details/143241972
-
1. 顺序结构顺序结构就是程序从上到下逐行地执行。表达式语句都是顺序执行的。并且上一行对某个变量的修改对下一行会产生影响。 public class StatementTest{public static void main(String[] args){int x = 1;int y = 2;System.out.println("x = " + x); System.out.println("y = " + y); //对x、y的值进行修改 x++; y = 2 * x + y; x = x * 10; System.out.println("x = " + x); System.out.println("y = " + y); }} Java中定义变量时采用合法的前向引用。如: public static void main(String[] args) {int num1 = 12;int num2 = num1 + 2;} 错误形式: public static void main(String[] args) {int num2 = num1 + 2;int num1 = 12;} 2. 分支语句2.1 if-else条件判断结构2.1.1 基本语法结构1:单分支条件判断:if 格式: if(条件表达式){ 语句块;} 说明:条件表达式必须是布尔表达式(关系表达式或逻辑表达式)或 布尔变量。 执行流程: 首先判断条件表达式看其结果是true还是false如果是true就执行语句块如果是false就不执行语句块 结构2:双分支条件判断:if…else 格式: if(条件表达式) { 语句块1;}else { 语句块2;} 执行流程: 首先判断条件表达式看其结果是true还是false如果是true就执行语句块1如果是false就执行语句块2 结构3:多分支条件判断:if…else if…else 格式: if (条件表达式1) { 语句块1;} else if (条件表达式2) { 语句块2;}...}else if (条件表达式n) { 语句块n;} else { 语句块n+1;} 说明:一旦条件表达式为true,则进入执行相应的语句块。执行完对应的语句块之后,就跳出当前结构。 执行流程: 首先判断关系表达式1看其结果是true还是false如果是true就执行语句块1,然后结束当前多分支如果是false就继续判断关系表达式2看其结果是true还是false如果是true就执行语句块2,然后结束当前多分支如果是false就继续判断关系表达式…看其结果是true还是false … n. 如果没有任何关系表达式为true,就执行语句块n+1,然后结束当前多分支。 当条件表达式之间是“互斥”关系时(即彼此没有交集),条件判断语句及执行语句间顺序无所谓。 当条件表达式之间是“包含”关系时,“小上大下 / 子上父下”,否则范围小的条件表达式将不可能被执行。 2.1.3 if…else嵌套在 if 的语句块中,或者是在else语句块中,又包含了另外一个条件判断(可以是单分支、双分支、多分支),就构成了嵌套结构。 执行的特点:(1)如果是嵌套在if语句块中的,只有当外部的if条件满足,才会去判断内部的条件(2)如果是嵌套在else语句块中的,只有当外部的if条件不满足,进入else后,才会去判断内部的条件 **案例4:**由键盘输入三个整数分别存入变量num1、num2、num3,对它们进行排序(使用 if-else if-else),并且从小到大输出。 class IfElseTest4 {public static void main(String[] args) { //声明num1,num2,num3三个变量并赋值int num1 = 23,num2 = 32,num3 = 12; if(num1 >= num2){ if(num3 >= num1)System.out.println(num2 + "-" + num1 + "-" + num3);else if(num3 <= num2)System.out.println(num3 + "-" + num2 + "-" + num1);elseSystem.out.println(num2 + "-" + num3 + "-" + num1);}else{ //num1 < num2 if(num3 >= num2){System.out.println(num1 + "-" + num2 + "-" + num3);}else if(num3 <= num1){System.out.println(num3 + "-" + num1 + "-" + num2);}else{System.out.println(num1 + "-" + num3 + "-" + num2);}}}} 2.1.4 其它说明语句块只有一条执行语句时,一对{}可以省略,但建议保留当if-else结构是“多选一”时,最后的else是可选的,根据需要可以省略2.2 switch-case选择结构2.2.1 基本语法语法格式: switch(表达式){ case 常量值1: 语句块1; //break; case 常量值2: 语句块2; //break; // ... [default: 语句块n+1; break; ]} 执行过程: 第1步:根据switch中表达式的值,依次匹配各个case。如果表达式的值等于某个case中的常量值,则执行对应case中的执行语句。 第2步:执行完此case的执行语句以后, 情况1:如果遇到break,则执行break并跳出当前的switch-case结构 情况2:如果没有遇到break,则会继续执行当前case之后的其它case中的执行语句。—>case穿透 … 直到遇到break关键字或执行完所有的case及default的执行语句,跳出当前的switch-case结构 使用注意点: switch(表达式)中表达式的值必须是下述几种类型之一:byte,short,char,int,枚举 (jdk 5.0),String (jdk 7.0); case子句中的值必须是常量,不能是变量名或不确定的表达式值或范围; 同一个switch语句,所有case子句中的常量值互不相同; break语句用来在执行完一个case分支后使程序跳出switch语句块; 如果没有break,程序会顺序执行到switch结尾; default子句是可选的。同时,位置也是灵活的。当没有匹配的case时,执行default语句。 2.2.3 利用case的穿透性在switch语句中,如果case的后面不写break,将出现穿透现象,也就是一旦匹配成功,不会在判断下一个case的值,直接向后运行,直到遇到break或者整个switch语句结束,执行终止。 案例:编写程序:从键盘上输入2023年的“month”和“day”,要求通过程序输出输入的日期为2023年的第几天。 import java.util.Scanner; class SwitchCaseTest4 {public static void main(String[] args) { Scanner scan = new Scanner(System.in); System.out.println("请输入2023年的month:");int month = scan.nextInt(); System.out.println("请输入2023年的day:");int day = scan.nextInt(); //这里就不针对month和day进行合法性的判断了,以后可以使用正则表达式进行校验。 int sumDays = 0;//记录总天数 //写法1 :不推荐(存在冗余的数据)/*switch(month){case 1:sumDays = day;break;case 2:sumDays = 31 + day;break;case 3:sumDays = 31 + 28 + day;break;//.... case 12://sumDays = 31 + 28 + ... + 30 + day;break;}*/ //写法2:推荐switch(month){case 12:sumDays += 30;//这个30是代表11月份的满月天数case 11:sumDays += 31;//这个31是代表10月份的满月天数case 10:sumDays += 30;//这个30是代表9月份的满月天数case 9:sumDays += 31;//这个31是代表8月份的满月天数case 8:sumDays += 31;//这个31是代表7月份的满月天数case 7:sumDays += 30;//这个30是代表6月份的满月天数case 6:sumDays += 31;//这个31是代表5月份的满月天数case 5:sumDays += 30;//这个30是代表4月份的满月天数case 4:sumDays += 31;//这个31是代表3月份的满月天数case 3:sumDays += 28;//这个28是代表2月份的满月天数case 2:sumDays += 31;//这个31是代表1月份的满月天数case 1:sumDays += day;//这个day是代表当月的第几天} System.out.println(month + "月" + day + "日是2023年的第" + sumDays + "天"); //关闭资源scan.close();}} 2.2.4 if-else语句与switch-case语句比较结论:凡是使用switch-case的结构都可以转换为if-else结构。反之,不成立。 开发经验:如果既可以使用switch-case,又可以使用if-else,建议使用switch-case。因为效率稍高。 细节对比: if-else语句优势if语句的条件是一个布尔类型值,if条件表达式为true则进入分支,可以用于范围的判断,也可以用于等值的判断,使用范围更广。switch语句的条件是一个常量值(byte,short,int,char,枚举,String),只能判断某个变量或表达式的结果是否等于某个常量值,使用场景较狭窄。switch语句优势当条件是判断某个变量或表达式是否等于某个固定的常量值时,使用if和switch都可以,习惯上使用switch更多。因为效率稍高。当条件是区间范围的判断时,只能使用if语句。使用switch可以利用穿透性,同时执行多个分支,而if…else没有穿透性。3. 循环语句理解:循环语句具有在某些条件满足的情况下,反复执行特定代码的功能。 循环结构分类: for 循环while 循环do-while 循环循环结构四要素: 初始化部分循环条件部分循环体部分迭代部分3.1 for循环3.1.1 基本语法语法格式: for (①初始化部分; ②循环条件部分; ④迭代部分){ ③循环体部分;} **执行过程:**①-②-③-④-②-③-④-②-③-④-…-② 说明: for(;;)中的两个;不能多也不能少①初始化部分可以声明多个变量,但必须是同一个类型,用逗号分隔②循环条件部分为boolean类型表达式,当值为false时,退出循环④可以有多个变量更新,用逗号分隔说明: 1、我们可以在循环中使用break。一旦执行break,就跳出当前循环结构。 2、小结:如何结束一个循环结构? 结束情况1:循环结构中的循环条件部分返回false 结束情况2:循环结构中执行了break。 3、如果一个循环结构不能结束,那就是一个死循环!我们开发中要避免出现死循环。 3.2 while循环3.2.1 基本语法语法格式: ①初始化部分while(②循环条件部分){ ③循环体部分; ④迭代部分;} **执行过程:**①-②-③-④-②-③-④-②-③-④-…-② 说明: while(循环条件)中循环条件必须是boolean类型。注意不要忘记声明④迭代部分。否则,循环将不能结束,变成死循环。for循环和while循环可以相互转换。二者没有性能上的差别。实际开发中,根据具体结构的情况,选择哪个格式更合适、美观。for循环与while循环的区别:初始化条件部分的作用域不同。3.3 do-while循环3.3.1 基本语法语法格式: ①初始化部分;do{③循环体部分④迭代部分}while(②循环条件部分); **执行过程:**①-③-④-②-③-④-②-③-④-…-② 图示: 说明: 结尾while(循环条件)中循环条件必须是boolean类型do{}while();最后有一个分号do-while结构的循环体语句是至少会执行一次,这个和for和while是不一样的循环的三个结构for、while、do-while三者是可以相互转换的。3.4 对比三种循环结构三种循环结构都具有四个要素:循环变量的初始化条件循环条件循环体语句块循环变量的修改的迭代表达式从循环次数角度分析do-while循环至少执行一次循环体语句。for和while循环先判断循环条件语句是否成立,然后决定是否执行循环体。如何选择遍历有明显的循环次数(范围)的需求,选择for循环遍历没有明显的循环次数(范围)的需求,选择while循环如果循环体语句块至少执行一次,可以考虑使用do-while循环本质上:三种循环之间完全可以互相转换,都能实现循环的功能3.5 "无限"循环 3.5.1 基本语法语法格式: 最简单"无限"循环格式:while(true) , for(;;)适用场景: 开发中,有时并不确定需要循环多少次,需要根据循环体内部某些条件,来控制循环的结束(使用break)。如果此循环结构不能终止,则构成了死循环!开发中要避免出现死循环。3.6 嵌套循环(或多重循环)3.6.1 使用说明所谓嵌套循环,是指一个循环结构A的循环体是另一个循环结构B。比如,for循环里面还有一个for循环,就是嵌套循环。其中,for ,while ,do-while均可以作为外层循环或内层循环。外层循环:循环结构A内层循环:循环结构B实质上,嵌套循环就是把内层循环当成外层循环的循环体。只有当内层循环的循环条件为false时,才会完全跳出内层循环,才可结束外层的当次循环,开始下一次的外层循环。设外层循环次数为m次,内层为n次,则内层循环体实际上需要执行m*n次。**技巧:**从二维图形的角度看,外层循环控制行数,内层循环控制列数。**开发经验:**实际开发中,我们最多见到的嵌套循环是两层。一般不会出现超过三层的嵌套循环。如果将要出现,一定要停下来重新梳理业务逻辑,重新思考算法的实现,控制在三层以内。否则,可读性会很差。例如:两个for嵌套循环格式 for(初始化语句①; 循环条件语句②; 迭代语句⑦) { for(初始化语句③; 循环条件语句④; 迭代语句⑥) { 循环体语句⑤; }} //执行过程:① - ② - ③ - ④ - ⑤ - ⑥ - ④ - ⑤ - ⑥ - ... - ④ - ⑦ - ② - ③ - ④ - ⑤ - ⑥ - ④.. **执行特点:**外层循环执行一次,内层循环执行一轮。 4. 关键字break和continue的使用4.1 break和continue的说明适用范围 在循环结构中使用的作用 相同点 break switch-case循环结构 一旦执行,就结束(或跳出)当前循环结构 此关键字的后面,不能声明语句 continue 循环结构 一旦执行,就结束(或跳出)当次循环结构 此关键字的后面,不能声明语句 此外,很多语言都有goto语句,goto语句可以随意将控制转移到程序中的任意一条语句上,然后执行它,但使程序容易出错。Java中的break和continue是不同于goto的。 4.2 应用举例class BreakContinueTest1 {public static void main(String[] args) { for(int i = 1;i <= 10;i++){ if(i % 4 == 0){//break;//123continue;//123567910//如下的语句不可能被执行,编译不通过//System.out.println("今晚迪丽热巴要约我吃饭");} System.out.print(i);} System.out.println("####"); //嵌套循环中的使用for(int i = 1;i <= 4;i++){ for(int j = 1;j <= 10;j++){if(j % 4 == 0){//break; //结束的是包裹break关键字的最近的一层循环!continue;//结束的是包裹break关键字的最近的一层循环的当次!}System.out.print(j);}System.out.println();} }} 4.3 带标签的使用break语句用于终止某个语句块的执行{ …… break;……} break语句出现在多层嵌套的语句块中时,可以通过标签指明要终止的是哪一层语句块 label1: { …… label2: { ……label3: { …… break label2; ……} }} continue语句出现在多层嵌套的循环语句体中时,也可以通过标签指明要跳过的是哪一层循环。 标号语句必须紧接在循环的头部。标号语句不能用在非循环语句的前面。 举例: class BreakContinueTest2 {public static void main(String[] args) {l:for(int i = 1;i <= 4;i++){ for(int j = 1;j <= 10;j++){if(j % 4 == 0){//break l;continue l;}System.out.print(j);}System.out.println();}}} 5. Scanner:键盘输入功能的实现如何从键盘获取不同类型(基本数据类型、String类型)的变量:使用Scanner类。 键盘输入代码的四个步骤: 导包:import java.util.Scanner;创建Scanner类型的对象:Scanner scan = new Scanner(System.in);调用Scanner类的相关方法(next() / nextXxx()),来获取指定类型的变量释放资源:scan.close();注意:需要根据相应的方法,来输入指定类型的值。如果输入的数据类型与要求的类型不匹配时,会报异常 导致程序终止。 5.1 各种类型的数据输入**案例:**小明注册某交友网站,要求录入个人相关信息。如下: 请输入你的网名、你的年龄、你的体重、你是否单身、你的性别等情况。 //① 导包import java.util.Scanner; public class ScannerTest1 { public static void main(String[] args) { //② 创建Scanner的对象 //Scanner是一个引用数据类型,它的全名称是java.util.Scanner //scanner就是一个引用数据类型的变量了,赋给它的值是一个对象(对象的概念我们后面学习,暂时先这么叫) //new Scanner(System.in)是一个new表达式,该表达式的结果是一个对象 //引用数据类型 变量 = 对象; //这个等式的意思可以理解为用一个引用数据类型的变量代表一个对象,所以这个变量的名称又称为对象名 //我们也把scanner变量叫做scanner对象 Scanner scanner = new Scanner(System.in);//System.in默认代表键盘输入 //③根据提示,调用Scanner的方法,获取不同类型的变量 System.out.println("欢迎光临你好我好交友网站!"); System.out.print("请输入你的网名:"); String name = scanner.next(); System.out.print("请输入你的年龄:"); int age = scanner.nextInt(); System.out.print("请输入你的体重:"); double weight = scanner.nextDouble(); System.out.print("你是否单身(true/false):"); boolean isSingle = scanner.nextBoolean(); System.out.print("请输入你的性别:"); char gender = scanner.next().charAt(0);//先按照字符串接收,然后再取字符串的第一个字符(下标为0) System.out.println("你的基本情况如下:"); System.out.println("网名:" + name + "\n年龄:" + age + "\n体重:" + weight + "\n单身:" + isSingle + "\n性别:" + gender); //④ 关闭资源 scanner.close(); }} 6. 如何获取一个随机数如何产生一个指定范围的随机整数? 1、Math类的random()的调用,会返回一个[0,1)范围的一个double型值 2、Math.random() * 100 —> [0,100)(int)(Math.random() * 100) —> [0,99](int)(Math.random() * 100) + 5 ----> [5,104] 3、如何获取[a,b]范围内的随机整数呢?(int)(Math.random() * (b - a + 1)) + a 4、举例 System.out.println(value); //[1,6]int number = (int)(Math.random() * 6) + 1; //System.out.println(number);}}class MathRandomTest {public static void main(String[] args) {double value = Math.random(); System.out.println(value); //[1,6]int number = (int)(Math.random() * 6) + 1; //System.out.println(number);}} System.out.println(value); //[1,6]int number = (int)(Math.random() * 6) + 1; //System.out.println(number);}}———————————————— 原文链接:https://blog.csdn.net/fj123789/article/details/145672376
推荐直播
-
华为开发者空间玩转DeepSeek
2025/03/13 周四 19:00-20:30
马欣 山东商业职业技术学院云计算专业讲师,山东大学、山东建筑大学等多所本科学校学生校外指导老师
同学们,想知道如何利用华为开发者空间部署自己的DeepSeek模型吗?想了解如何用DeepSeek在云主机上探索好玩的应用吗?想探讨如何利用DeepSeek在自己的专有云主机上辅助编程吗?让我们来一场云和AI的盛宴。
即将直播 -
华为云Metastudio×DeepSeek与RAG检索优化分享
2025/03/14 周五 16:00-17:30
大海 华为云学堂技术讲师 Cocl 华为云学堂技术讲师
本次直播将带来DeepSeek数字人解决方案,以及如何使用Embedding与Rerank实现检索优化实践,为开发者与企业提供参考,助力场景落地。
去报名
热门标签