• [技术干货] Java环境中系统属性和运行参数
    我们在开发Java项目的时候或多或少都会跟Java环境打交道,如何按照需求更改我们的环境配置达到我们所需要的环境呢?本文中将对这些进行简单的探究。1.系统属性的简介系统属性就是系统级全局变量,该参数在程序中任何位置都可以访问到。优先级是最高的,该参数的使用会覆盖程序中的同名配置(相当霸道)。设置系统属性的标准格式为:-Dargname=argvalue,如果有多个参数的话它们之间需要使用空格来进行分隔,如果参数值中间存在空格,则需要使用引号来将其括起来。 其中的参数名可以是 Java 默认的这样这些参数就可以由 JVM 虚拟机来自动识别并且生效了。举个栗子: -Dfile.encoding=UTF-8 是用于指定文件编码格式,也可以是自定义的参数,另外个栗子:-Dtest=123,设置完后程序中是可以读取到该参数值的并且执行相关的逻辑操作。那么虚拟机是可以识别了,那么我们如何来进行获取呢,我们获取虚拟机中系统参数设置的参数键值对,可以在程序中使用System.getProperty("propertyName") 方法来进行获取对应的参数值,并且可以在程序中通过System.setProperty(k,v)方法来设置系统参数,其中参数-X/-XX 为非标准系统参数形式,一般与 JVM 虚拟机设置有关,参数名和值都由JVM 规范来规定。比如:-Xms :初始堆大小、-Xmx :最大堆大小。常用的方法中System.getProperty(XXX)是用来获取系统属性的,而System.getEnv(XXX)方法则是用来获取系统环境变量的。比如可以再IDEA中的Configuration中VM options设置-Dtest.aaa=123,然后再在项目中使用System.getProperty("test.aaa")就可以获取到123这个值了。2 运行参数main方法中执行时传入的参数值,如果参数有多个是通过空格来进行分隔的。main 方法的一般格式是:public static void main(String[] args),其中,Stringp[] args 就是存储运行参数的变量,在程序中可以直接使用。
  • [技术干货] Mybatis-Plus中小知识点
    1.MyBatis-Plus简介MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。在开发过程中,MyBatis-Plus给我们带来了很多的遍历,一些简单逻辑的sql操作可以不用再写sql通过简单的一两行代码就可以完成。2.MyBatis-Plus的使用MyBatis-Plus在开发过程中的使用常常跟lombok一起使用,通过lombok来简化实体类的创建。然后通过创建自己的IBaseService 继承Mybatis-Plus提供的基类,最后用自己创建的service对象调用,就可以便捷的调用MyBatis-Plus中封装好的各种实用的CRUD方法了(插入、更新、删除、根据id插入、根据id更新、根据id删除、批量处理数据等),我们还可以传入创建的Wrapper对象来添加各种筛选条件。@Datapublic class User { private Long id; private String name; private Integer age;}...// 插入boolean save(T entity);// 批量插入boolean saveBatch(Collection entityList);// 批量插入,每次插入数量boolean saveBatch(Collection entityList, int batchSize);...// 更新或插入boolean saveOrUpdate(T entity);...// 根据UpdateWrapper条件更新boolean update(Wrapper updateWrapper);// 根据ID更新boolean updateById(T entity);// 根据ID批量更新boolean updateBatchById(Collection entityList);3.MyBatis-Plus存在的小问题在Mybatis-Plus中调用updateById方法进行数据更新默认情况下是不能更新空值字段的,而在实际开发过程中,经常会遇到需要将字段值更新为空值的情况。那么我们应该怎么去改进呢?因为Mybatis-Plus中字段的更新策略是通过FieldStrategy属性控制的,它是在实体字段上,如果不通过@TableField注解指定字段的更新策略,字段默认的更新策略就是FieldStrategy.DEFAULT,即跟随全局策略。 而Mybatis-Plus的全局配置里字段的默认更新策略是FieldStrategy.NOT_NULL是进行空值判断的,不会对NULL值数据进行处理的。 知道了问题那么我们就可以通过问题去探索解决方案了,我们可以设置字段级别的更新策略为IGNORED就可以了,只需要在实体中字段上面通过@TableField注解指定字段的更新策略为FieldStrategy.IGNORED,忽略空值判断直接进行更新就可以了。@TableField(updateStrategy = FieldStrategy.IGNORED) private String name;还可以设置全局更新策略的IGNORED ,通过修改Mybatis-Plus的项目级别的全局更新策略来进行控制, 在spring boot中只需要修改属性: mybatis-plus.global-config.db-config.update-strategy=ignored就可以完成策略的变更。以上就是Mybatis-Plus中小知识点的总结。
  • [其他] SMC3.0 添加组织的接口
    这个添加组织的接口,调了也没添加上去,然后也不报错,返回的结果是查询所有组织的接口的返回值
  • [Java] N皇后问题-二进制解法
    @Test public void testNQueen() { int n = 14; int limit = n == 32 ? -1 : (1 << n) - 1; System.out.println(binaryNQueen(limit, 0, 0, 0)); } /** * n皇后问题 二进制解法 * @param limit 皇后规模 * @param cloLimit 列限制 * @param lLimit 左斜线限制 * @param rLimit 右斜线限制 * @return */ public int binaryNQueen(int limit, int cloLimit, int lLimit, int rLimit) { if (cloLimit == limit) { return 1; } int tempLimit = ~(cloLimit | lLimit | rLimit) & limit; int temp; int res = 0; while (tempLimit != 0) { temp = tempLimit & (~tempLimit + 1); tempLimit = tempLimit - temp; res += binaryNQueen(limit, cloLimit | temp, (lLimit | temp) << 1, (rLimit | temp) >>> 1); } return res; }
  • [技术干货] spring boot常用配置属性
    1.springbootspringboot框架目前是最常用的Java Web项目快速开发框架,我们的技术选型也常常围绕着springboot来进行搭配搭建,而我们在搭建的过程中需要在.properties或者.yml配置文件中进行一些简单的属性配置,本文中就是对这些配置进行归纳总结。2.应用相关的配置配置服务器HTTP端口可以应用server.port = 端口号用于Server响应头的值(如果为空,则不发送头)可以应用server.server-header =是否应将X-Forwarded- *头应用于HttpRequest可以应用server.use-forward-headers =Servlet context init参数可以应用server.servlet.context-parameters .*=应用程序的上下文路径可以应用server.servlet.context-path =主调度程序servlet的路径可以应用server.servlet.path = /会话cookie的最大有效时间,如果未指定持续时间后缀,则将使用秒可以应用server.servlet.session.cookie.max-age =会话超时。如果未指定持续时间后缀,则将使用秒可以应用server.servlet.session.timeout =连接器在关闭连接之前等待另一个HTTP请求的时间,未设置时默认使用连接器的默认值。使用值-1表示没有(即无限)超时可以应用server.connection-timeout =错误控制器的路径可以应用server.error.path = /error3.log4j2日志相关配置日志配置文件的位置logging.config =记录异常时使用的转换字logging.exception-conversion-word =xxx 。日志文件名logging.file = 要保留的归档日志文件的最大值logging.file.max-history = 0最大日志文件大小logging.file.max-size = 100MB日志级别严重性映射logging.level.* = DEBUG日志文件的位置logging.path = 用于输出到控制台的Appender模式logging.pattern.console =以上是Springboot 2.0版本使用的相关配置属性,下面我们介绍下Spring2.2版本使用的一些配置变更。4.Springboot 2.2配置变更Springboot 2.2有很多的变更主要的性能提升在:绑定大量配置属性所需的时间已大大减少;当Spring Boot PersistenceUnit通过扫描JPA实体完全准备一个时,由于它是冗余的,因此Hibernate自己的实体扫描已被禁用;自动配置中的注入点已经过改进,仅适用于必须创建bean的情况;现在仅在启用和公开端点的情况下(通过JMX或HTTP)创建与Actuator端点相关的Bean; 编解码器自动配置的条件已得到改善,以便在不再使用编解码器时不再对其进行配置;Tomcat的MBean注册表默认情况下处于禁用状态,从而将Tomcat的内存占用量减少了大约2MB等 配置文件的变更有:将logging.file属性变更为logging.file.name将logging.path属性变更为logging.file.pathserver.connection-timeout不再建议使用该属性了 将server.use-forward-headers=true属性变更为server.forward-headers-strategy=native将agentMaven属性变更为agents将WebTestClientBuilderCustomizer移动到org.springframework.boot.test.web.reactive.server
  • [Java] Java多线程编程(线程的常用操作)
    线程常用操作线程的命名和取得线程的命名和取得都是来源于Thread类中线程名称的操作常用有一下三种操作构造方法命名:public Thread(Runnalbe target,String name) ;setName()方法命名:public final void setName(String name);getName()方法取得名字:public final String getName();范例:在run()方法中获取线程名称class mythread implements Runable{ @Override public void run{ System.out.println(Thrad.currentThread().getName()); } } public class ThreadDemo{ public static void main(String[] args) throws Exception{ mythread mt=new mythread(); new Thread(mt,"线程A").start();//设置了线程名字 new Thread(mt).start();//未设置线程名字 new Thread(mt,"线程B").start();//设置了线程名字 } }获取线程名称时,未命名线程会自动分配名称 接下来获取主线程名称class mythread implements Runable{ @Override public void run{ System.out.println(Thrad.currentThread().getName()); } } public class ThreadDemo{ public static void main(String[] args) throAws Exception{ mythread mt=new mythread(); new Thread(mt,"线程对象").start();//设置了线程名字 mt.run();//对象直接调用run()方法 } }==获取主线程名称为main(主方法也是线程)==线程休眠我们可以希望某个线程能够暂缓执行以方便观察运行状态在进行休眠的时候有可能会产生在进行休眠的时候有可能会产生 中断异常“InterruptedException”,中断异常属于Exception的子类,证明该异常必须进行处理==休眠方法:==休眠:public static void sleep(long millis) throws InterruptedException;休眠:public static void sleep(long mills,int nanos) throws InterruptedException;范例:观察休眠处理(单个对象进行线程休眠处理)public class ThreadDemo{ public static void main(String[] args) throws Exception{ new Thread(()->{ for(int x=0;x<10;x++) { System.out.println(Thread.currentThrad().getName()+",x="+x); try{ Thread.sleep(1000);暂缓执行 } catch(InterruptedException e){ e.printStackTrace(); } } },"线程对象").start(); } }在线程的启动上有线程后,当程序休眠再启动也有先后,就像接力比赛,接棒时当作休眠,休眠后每个人的运行速度是不相同的,这就造成了线程执行完毕的先后顺序也是不同的范例:产生多个线程对象进行线程处理public class ThreadDemo{ public static void main(String[] args) throws Exception{ Runnable run=()->{ for(int x=0;x<10;x++) { System.out.println(Thread.currentThrad().getName()+",x="+x); try{ Thread.sleep(1000);暂缓执行 } catch(InterruptedException e){ e.printStackTrace(); } } }; for(int num=0;num<5;num++) { new Thread(run,"执行线程"+num).start(); } } }线程休眠要通过运行代码自己观察线程中断在之前的线程休眠中发现里面提供有一个中断异常,这实际上证明线程休眠是可以被打断的,这种打断肯定是由其他线程完成的在Thread类里面提供有线程处理方法判断线程是否被中断:public boolean isInterrupted();中断线程执行:public void interrupt();范例:观察线程的中断处理public class ThreadDemo { public static void main(String[] args) throws Exception { Thread thread=new Thread(()->{ System.out.println("*****我需要休眠*****"); try { Thread.sleep(10000);//休息10秒 System.out.println("***睡足了,可以去工作了***"); } catch (InterruptedException e) { // TODO Auto-generated catch block System.out.println("休息被打断"); } }); thread.start();//开始休息 Thread.sleep(1000);//先休息1秒钟 if(!thread.isInterrupted()) {//询问是否中断休息 //没有中断 thread.interrupt();//现在打断你的休息 } } }正在执行的线程都是可以被中断的,中断的线程必须进行异常处理线程强制运行当满足某些条件之后,某一个线程对象可以一直独占资源一直到线程结束范例:观察一个没有强制执行的程序public class ThreadDemo { public static void main(String[] args) throws Exception { Thread thread=new Thread(()->{ for(int x=0;x<100;x++) { try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"执行、x="+x); } },"玩耍的线程"); thread.start(); for(int x=0;x<100;x++) { Thread.sleep(100); System.out.println("霸道的main线程执行x="+x); } } }我们发现在子线程会和主线程抢占资源进行输出,接下来我们看看在利用了线程的强制执行后的运行状态在Thread类里面提供有强制执行方法,join()方法==public final void join() throws InterruptedException==利用join()方法我们可以使线程强制执行独占资源从强制执行抛出异常看出,强制执行也可以被中断范例:强制执行程序public class ThreadDemo { public static void main(String[] args) throws Exception { Thread mainThread=new Thread().currentThread();//获取主线程 Thread thread=new Thread(()->{ for(int x=0;x<100;x++) { try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } if(x>3) { try { mainThread.join();//霸道的线程开始执行 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.println(Thread.currentThread().getName()+"执行、x="+x); } },"玩耍的线程"); thread.start(); for(int x=0;x<100;x++) { Thread.sleep(100); System.out.println("霸道的main线程执行x="+x); } } }可以看出当下x>3后main()开始独占资源,当main()执行完成后其他线程才开始执行在进行强制执行的时候一定要获取强制执行线程对象才可以进行join的操作线程礼让在多线程中,可以先将资源让出去让别的线程先执行,线程的礼让可以用Thread中的方法礼让方法:public static void yield();范例:线程的礼让执行public class ThreadDemo { public static void main(String[] args) throws Exception { Thread thread=new Thread(()->{ for(int x=0;x<100;x++) { if(x%3==0) { Thread.yield();//玩耍的线程礼让了 System.out.println("****玩耍的线程礼让了*****"); } try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"执行、x="+x); } },"玩耍的线程"); thread.start(); for(int x=0;x<100;x++) { Thread.sleep(100); System.out.println("霸道的main线程执行x="+x); } } }每一次调用yield()都只会礼让一次当前的资源线程优先级从理论上讲线程的优先级越高越有可能先执行(越有可能先抢占到资源)在Thread类里面提供有如下的两个处理方法设置优先级:public final void setPriority(int newPriority);获取优先级:public final int getPriority(); 在进行优先级定义的时候都是通过int型的数字来进行完成的,而对于此类数字的选择在Thread类里面就有定义最高优先级:public static final int MAX_PRIORITY、10(优先级的值)中等优先级:public static final int NORM_PRIORITY、5(优先级的值)最低优先级:public static final int MIN_PRIORITY、1(优先级的值)范例:没有设置优先级public class ThreadDemo { public static void main(String[] args) throws Exception { Runnable run=()->{ for(int x=0;x<10;x++) { try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"执行"); } }; Thread threadA=new Thread(run,"线程A对象"); Thread threadB=new Thread(run,"线程B对象"); Thread threadC=new Thread(run,"线程C对象"); threadA.start(); threadB.start(); threadC.start(); } }不同电脑效果不同,本人的是B优先执行 接下来看使用优先级后电脑执行的效果 范例:观察设置优先级后的效果public class ThreadDemo { public static void main(String[] args) throws Exception { Runnable run=()->{ for(int x=0;x<10;x++) { try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"执行"); } }; Thread threadA=new Thread(run,"线程A对象"); Thread threadB=new Thread(run,"线程B对象"); Thread threadC=new Thread(run,"线程C对象"); threadA.setPriority(Thread.MIN_PRIORITY);//把AB设置优先级最小 threadB.setPriority(Thread.MIN_PRIORITY);//把AB设置优先级最小 threadC.setPriority(Thread.MAX_PRIORITY);//把AB设置优先级最小 threadA.start(); threadB.start(); threadC.start(); } }提高有先级确实可以提高线程先执行的可能,但是并不是绝对先执行补充:主方法是一个主线程,主线程的优先级是多少呢?public class ThreadDemo { public static void main(String[] args) throws Exception { System.out.println(Thread.currentThread().getPriority()); } }发现主线程的优先级是5,只是中等优先级,同时这与我们默认的方法的优先级是相同的
  • [技术干货] 时间转换的知识总结
    1.时间类型的转换问题时间类型的转换问题一直是我们开发中经常接触的一个问题,涉及到的问题有字符串转时间,时间转字符串等等,经常用,又总是记不太清,本文中对这些时间类型的相关转换进行记录和总结。2.字符串和Date类型互相转换2.1 利用SimpleDateFormat转换通过java.text.SimpleDateFormat类进行转换,这个类中有许多专门用来实现时间和字符串之间的互相转换的方法。首先需要创建格式化的对象,并且设置需要格式化的样式,比如可以用"yyyy-MM-dd HH:mm:ss","yyyy/MM/dd HH:mm:ss","yyyy年MM月dd日 HH时mm分ss秒"等等这种全部动态变化的格式,也可以指定一部分动态换行一部分不变的样式,比如可以用“yyyy-MM-dd 10:00:00”,"2022-MM-dd HH:mm:ss"等等格式。SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");2.1.1 时间对象转字符串: 通过sdf.format(需要转换的时间对象); 格式化方法,可以将日期对象转换为我们需要的时间格式的字符串形式,返回的时间字符串的格式是由上面创建对象时定义格式来决定的。2.1.2 时间字符串转日期对象:    首先需要保证字符串的格式也和上面我们创建的格式化对象sdf实例中定义的格式一样, 然后通过sdf.parse("需要转换的时间字符串");方法,就可以将时间字符串转换成时间对象了。2.2 利用DateUtil转换还可以通过import cn.hutool.core.date.DateUtil类中的专门转换时间的方法进行转换。2.2.1 时间对象转时间字符串通过String time = DateUtil.format(new Date(),"yyyyMMddHHmmss");方法可以将时间对象转换为指定格式的时间格式的字符串,第二个参数就是需要转换的时间格式。2.2.2 时间字符串转时间对象通过Date time = DateUtil.parse(待转化的时间格式字符串, "yyyyMMddHHmmss");方法以将时间形式的字符串转换为指定格式的时间格式的对象,第二个参数就是需要转换的时间格式,需要注意的是待转化的字符串的格式需要跟第二个参数的时间格式保持一致。2.2.3 增加时间和减少时间可以通过Date 减少后的时间 = DateUtil.offsetMinute(time, -时间);方法将时间对象传入第一个参数,将负整数类型传入第二个参数,表示减少多少分钟。Date 增加后的世界 = DateUtil.offsetMinute(time, 时间);方法将时间对象传入第一个参数,将正整数类型传入第二个参数,表示增加多少分钟。类似的还有增加减少秒.offsetSecond(),增加减少小时offsetHour(),增加减少天.offsetDay(),增加减少月offsetMonth(),增加减少年.offsetfYear(),都可以利用这个类中的方法便捷的进行增减操作。以上就是对时间转换小知识点简单归纳和总结。
  • [技术干货] IDEA使用中的小技巧
    1.IDEA简介IDEA全称是IntelliJ IDEA,它是JetBrains公司推出一款集成开发工具,在Java开发工具中的由于其集成了很多应用插件,比如提交代码的Git,依赖管理的Maven,项目启动用到的Tomcat等等,就是因为相对于其它开发工具来说IDEA提供了这些非常强大的黑科技,所以尽管有各种各样的开发工具,开发者们还是越来越受开发者的青睐这款可以非常便捷的开发我们的Java相关项目的开发工具。2.IDEA存在的问题成也萧何败也萧何,也正是因为IDEA集成了各式各样的插件以及各种便捷开发的功能,使得我们的开发变得更加轻松和舒适,也造成了IDEA会占用很多内存,有种很笨重的感觉,并且由于服务多了往往会有一些照顾不到的地方,我们在使用的过程中往往会碰到各种各样奇怪的问题出现,下面本文中主要总结一下修改的修改代码重启不生效的解决方法。3.IDEA使用中的修改的修改代码重启不生效的解决方法问题描述:明明修改了代码,但是重启后修改的代码却灵异的失效了,代码看着好的,就是不起作用,感觉就像是有缓存一样,面对这个问题应该如何去解决呢?可以通过maven clean清理项目后再重新打包项目来解决,但是这样就感觉很麻烦,并且如果忘记清理后再打包,代码还是会没有生效,也很难排查到,会造成很多不必要的麻烦。我们可以通过如下方法进行尝试解决,毕竟个人的环境和配置不同,有些在一台机器上生效了,在另一台又不行了,所以多准备几个解决方法有备无患,解决方法如下:方法1:右上角-Edit Configurations中,设置Update选项为Update classes and resources,如果不行继续进行下个方法;方法2:Ctrl+Shift+Alt+/ 四建齐按弹出框选择Registry,选中打勾 “compiler.automake.allow.when.app.running” 。方法3 在左上角“File”--> “Settings”--> “Build,Execution,Deplyment”--> “Compiler”中勾选中 “Build project automatically” 。方法4:删除项目的.idea文件,关闭IDEA后再重新打开IDEA后生效。以上就是IDEA使用中出现修改代码不生效的小技巧的归纳总结。
  • [Java] pagehelper查询慢问题
    pagehelper查询慢是因为,这个插件会自动在原来的查询逻辑上加一个count的sql,导致整体查询变慢。推荐方案是覆盖原来的SQL,重新自己写一个count sql覆盖只需要在查询的mapper文件中,写一个已 _COUNT结尾的查询就会覆盖默认的。selectActivityList 是查询语句的名称
  • [问题求助] 好望云媒体播放管理 需要配合哪个SDK使用在哪里引入
    SDK是要下载本地jar包还是maven直接引入的SDK,哪里可以找到 #java
  • [技术干货] 推荐一个YANG编译工具,可以解决编译YANG的时候必须找全依赖文件的问题
    开源项目地址:cid:link_0当我们写了一个YANG文件,想要验证一下YANG文件正确性的时候,所有的YANG解析器都会要求用户准备好该YANG文件的依赖文件,比如a模块依赖了b模块,那么在编译的时候用户需要自己去找到b模块的文件,但是YANG文件的依赖关系非常复杂,用户很难一下子找全,这样就会经常出现编译失败的事情。yang compiler支持自动分析YANG文件依赖关系,当编译目录不存在依赖文件时,会自动从网络上下载匹配的YANG文件(目前默认从yang catalog上下载),这些文件可以缓存在用户本地目录中,下次再用到时就无需再次下载了。这样yang compiler就支持无论待编译文件在哪里,都可以成功编译YANG文件,无需自己寻找依赖文件。另外yang compiler还支持插件系统,开发者可以按照yang compiler的要求开发个性化的扩展功能,比如生成yang tree或者根据YANG生成java或者python的类等等。
  • Java是如何制作月饼的——制作、下单和售卖-转载
     本文导读 中秋节是中国民间的传统节日,中秋节源自天象崇拜由上古时代秋夕祭月演变而来。中秋节自古便有祭月、赏月、吃月饼等民俗,流传至今,经久不息。 说到月饼,我们现在吃的都是工厂、小作坊、自己家里制作,在电商平台售卖或者有一些营销活动赠送,我们来看看Java是怎么制作月饼的,我们是否可以在代码实现一个月饼售卖的架构设计和设计模式。 借此机会,我们用Lambda实现一遍月饼制作,下单,售卖的开发设计模式,主要有制作月饼的工厂模式、 一、使用工厂模式制作月饼 设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、以及解决问题的思维方式。使用工厂模式,我们就无需向消费者们暴露,我们的月饼(实例化月饼对象)的制作逻辑,就能完成月饼的制作。  1、使用工厂创建月饼实例 我们需要一个月饼工厂(MoonCakeFactory)或者叫月饼小作坊,在工厂里我们可以创建一个map 将产品名映射到对应的构造函数中,提供一个像工厂模式一样,利用map来实例化不同对象。  /**  * 一个简单的月饼制作工厂  * @author XiaoMing  */ public class MoonCakeFactory {     /**      * 我们可以创建一个map 将产品名映射到对应的构造函数中      */     final static Map> map = new HashMap<>();       static {         // 五仁的,我的最爱         map.put("wuRen", WuRenMoonCakeInfo::new);         // 豆沙         map.put("redBeanPaste", RedBeanPasteMoonCakeInfo::new);         // 枣泥         map.put("jujubePaste", JujubePasteMoonCakeInfo::new);         // 蛋黄的         map.put("eggYolk", EggYolkMoonCakeInfo::new);     }       /**      * 像工厂模式一样,利用map来实例化不同对象      *      * @param moonCakeType 月饼类型      */     public static MoonCakeInfo createMoonCake(String moonCakeType) {         // 获取月饼供应商         Supplier moonCakeInfoSupplier = map.get(moonCakeType);         if (null != moonCakeInfoSupplier) {             return moonCakeInfoSupplier.get();         }         throw new IllegalArgumentException("No such MoonCakeInfo" + moonCakeType);     } }   /**  * @author XiaoMing  */ public class WuRenMoonCakeInfo extends MoonCakeInfo {     private String wuRen;     public String getWuRen() { return wuRen; }     public void setWuRen(String wuRen) { this.wuRen = wuRen; } }  2、工厂模式剖析  现在我们就可以试着拿到对应的,实例化的月饼对象了 WuRenMoonCakeInfo wuRen = (WuRenMoonCakeInfo) createMoonCake("wuRen"); 我们看完了这个例子,现在思考内部的问题,这么做有什么好处?为什么createMoonCake是一个static的方法? 我们制作月饼有很多馅,除了馅我们还可以取对应馅(moonCakeType)的时候,就可以获取到一个实例化的对象,不需要我们在使用的地方创建,将工厂定义为静态方法是一个常用技巧,称为静态工厂,不需要创建方法来实例化对象,将实例化对象和创建方法解耦。 总结一下,工厂模式是用一个专业类(工厂类)来负责一种产品的对象创建。这样做的好处是,1、把对象的创建和使用分开;2、将生产过程集中后,便于集中管理(增删改);3、当实体类有变动时,使用者不需要再去修改代码。 二、使用策略模式限制购买月饼类型 一个礼盒中有很多类型的月饼,有些馅并不我待见,博主曾经吃过一个咖啡馅月饼,这里面我们把奇奇怪怪的馅扣除去。校验是否为有效订单,设置该策略不允许咖啡馅月饼和榴莲馅的下单  1、实战代码 策略模式可以理解为一种通过算法解决一类问题的通用方案,策略模式包括该算法的接口,一个或多个接口的实现逻辑,以及策略对象,下面我们用lambda表达式实现,Validator 相当于一个中转站,strategy.validatorOrder(orderType);可以通过下属lambda实现,也可以通过实现类,继承Strategy 接口实现      /**      * 假设为下单主流程      * 我们主要关注 策略模式下订单类型的校验,这几个步骤      */     public void submitOrder(OrderInfo orderInfo) {         // ...         // 校验是否为有效订单,设置该策略不允许咖啡馅月饼和榴莲馅的下单         Validator v1 = new Validator(orderType -> !orderType.equals("Coffee") || !orderType.equals("Durian"));         v1.validatorOrder(orderInfo.getOrderType());         // ...     }     /**  * @author XiaoMing  */ public class Validator {     private Strategy strategy;       public Validator(Strategy strategy) {         this.strategy = strategy;     }       public boolean validatorOrder(String orderType) {         return strategy.validatorOrder(orderType);     } }     /**  * @author XiaoMing  */ public interface Strategy {     /**      * 函数式接口,通过调用 Validator时实现      */     boolean validatorOrder(String orderType); }   一般我们也可以使用实现类实现  /**  * @author xiaoming  * @date 2022/8/27-10:33  */ public class RealOrderImpl implements Strategy {     @Override     public boolean validatorOrder(String orderType) {         System.out.println("real");         return orderType.equals("real");     } }   public class O2OOrderImpl implements Strategy {     @Override     public boolean validatorOrder(String orderType) {         System.out.println("O2O");         return orderType.equals("O2O");     } }  2、策略模式剖析 策略模式需要做的就是当请求进来时候,同一个入口让他根据这个人请求的行为去执行其中某一个类中的方法。 策略接口的定义,通常包含两个方法:获取策略类型的方法和处理策略业务逻辑的方法。策略接口的实现,每种支付类都实现了上述接口(基于接口而非实现编程),这样我们可以灵活的替换不同的支付方式(上文代码段就是使用lambda表达式实现的)。 接口只负责业务策略的定义,每个策略的具体实现单独放在实现Impl中,我们可以使用工厂类 Factory 只负责获取具体实现类,而具体调用代码则负责业务逻辑的编排。这些实现用到了面向接口而非实现编程,满足了职责单一、开闭原则,从而达到了功能上的高内聚低耦合、提高了可维护性、扩展性以及代码的可读性。 三、模板方法模式设计月饼的优惠决策 我们购买月饼在电商业务中有很多优惠信息,例如红包、平台券、商家券、满减、秒杀活动等等, 如果每个优惠写一个方法那么就不方便阅读和管理,我们可以用这种方式将代码扩展性做的很好假设为下单主流程,我们主要关注 优惠决策与核销->优惠信息落库,这几个步骤。 1、使用模板方法实现优惠活动 让我们从模板方法模式设计月饼的优惠决策的例子着手,看看这个模式是如何工作的。     /**      * 假设为下单主流程      * 我们主要关注 优惠决策与核销->优惠信息落库,这几个步骤      */     public void submitOrder(List orderList) {         // ...         // 优惠决策与核销         writeOff(orderList, couponInfos -> {             // 将优惠信息落库             transaction(orderList);         });         // ...     }         /**      * 我们购买月饼在,电商业务中有很多优惠信息,例如红包、平台券、商家券、满减、秒杀活动等等      * 如果每个优惠写一个方法那么就不方便阅读和管理,我们可以用这种方式将代码扩展性做的很好      */     public void writeOff(List orderList, Consumer> consumer) {         // 每个优惠信息,由自己的实现类实现         List couponInfos = consume(orderList);           // 此处将结果保存         consumer.accept(couponInfos);     }         /**      * 每个优惠信息自己实现 ServiceImpl      */     private List consume(List orderList) {         List couponInfos = new ArrayList<>();         // 核销后返回信息(实现业务逻辑)         // ...         return couponInfos;     }         /**      * 将优惠信息落库      */     private static void transaction(List orderInfos) {       } 2、模板方法剖析 模板方法模式,如果你需要采用某个算法的框架,同时又希望有一定的灵是活度,能对它的某些部分进行改进,那么采用模板方法设计模式是比较通用的方案。这样讲有些抽象。换句话说,模板方法模式在你希望使用这个算法,但是需要对其中的某些行进行改进,才能达到希望的效果时是非常有用的。   优点,1、提高代码复用性,可以将相同部分的代码放在抽象的父类中;2、提高了拓展性,将不同的代码放入不同的子类中,通过对子类的扩展增加新的行为;3、实现了反向控制,通过一个父类调用其子类的操作,通过对子类的扩展增加新的行为。 缺点,1、引入了抽象类,每一个不同的实现都需要一个子类来实现,导致类的个数增加,从而增加了系统实现的复杂度。 场景,1、一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现;2、各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。  总结 借此中秋节机会,我们实现一个月饼售卖的设计模式。主要使用了Lambda和设计模式的思想,实现月饼制作的工厂模式,下单的策略模式校验,优惠券操作的模板方法模式的开发。 设计模式是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 项目中合理的运用设计模式可以完美的解决很多问题,每种模式在现在中都有相应的原理来与之对应,每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是它能被广泛应用的原因。 ———————————————— 版权声明:本文为CSDN博主「小明java问道之路」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/FMC_WBL/article/details/126533844 
  • 剑指 Offer 49. 丑数 && 264. 丑数 II ●●-转载
     描述 只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。  通常 1 为第一个丑数,求按从小到大的顺序的第 n 个丑数。  示例 输入:n = 10 输出:12 解释:[1, 2, 3, 4, 5, 6, 8, 9, 10, 12] 是由前 10 个丑数组成的序列。  题解 1. 小顶堆 要得到从小到大的第 n 个丑数,可以使用最小堆实现。  初始时堆为空。首先将最小的丑数 1 加入堆。  每次取出堆顶元素 x,即堆中最小的丑数,由于 2x, 3x, 5x 也是丑数,因此将 2x, 3x, 5x 加入堆。  上述做法会导致堆中出现重复元素的情况。为了避免重复元素,可以使用哈希集合去重,避免相同元素多次加入堆。  在排除重复元素的情况下,第 n 次从最小堆中取出的元素即为第 n 个丑数。  时间复杂度:O ( n log ⁡ n ) O(n \log n)O(nlogn)。得到第 n 个丑数需要进行 n 次循环,每次循环都要从最小堆中取出 1 个元素以及向最小堆中加入最多 3 个元素,因此每次循环的时间复杂度是 O ( log ⁡ ( 3 n ) + 3 log ⁡ ( 3 n ) ) O(\log (3n) + 3 \log (3n))O(log(3n)+3log(3n)),总时间复杂度是 O ( n log ⁡ n ) O(n \log n)O(nlogn)。 空间复杂度:O ( n ) O(n)O(n)。空间复杂度主要取决于最小堆和哈希集合的大小,最小堆和哈希集合的大小都不会超过 3n。 class Solution { public:     int nthUglyNumber(int n) {         vector factors = {2, 3, 5};        // 因子         unordered_set hash;                // 哈希         priority_queue, greater> heap;    // 小顶堆         hash.insert(1L);         heap.push(1L);         int uglyn = 1;                                     for(int i = 1; i <= n; ++i){             long top = heap.top();             heap.pop();             uglyn = (int)top;                    // 第n个丑数             for(int factor : factors){            // 堆中最小的丑数 * 因子2/3/5                 long next = top * factor;                 if(hash.count(next) == 0){                     hash.insert(next);                     heap.push(next);                 }             }         }         return uglyn;     } }; 2. 动态规划 方法一使用最小堆,会预先存储较多的丑数,维护最小堆的过程也导致时间复杂度较高。 可以使用动态规划的方法进行优化。  定义数组 uglys,其中 uglys[i-1] 表示第 i 个丑数。 uglys[0] = 1;  如何得到其余的丑数呢? 每个丑数都有一次与 2/3/5 相乘的机会,定义三个指针 p2、p3、p5,表示下一个丑数是三个指针指向的丑数乘以对应的质因数得到的最小值。初始时,三个指针的值都是 0,每轮将对应指针执行 +1 即可。  uglys[i] = min(min(num2, num3), num5); 在进行指针判断移动操作时,对每个指针得到的结果都进行一次对比,去除重复操作,比如 5*2 与 2*5,那么 p2 和 p5 都右移。  时间复杂度:O(n)。需要计算数组中的 n 个元素,每个元素的计算都可以在 O(1) 的时间内完成。 空间复杂度:O(n) class Solution { public:     int nthUglyNumber(int n) {         vector uglys(n, 1);         int p2 = 0, p3 = 0, p5 = 0;         for(int i = 1; i < n; ++i){             int num2 = uglys[p2]*2, num3 = uglys[p3]*3, num5 = uglys[p5]*5;             uglys[i] = min(min(num2, num3), num5);             if(uglys[i] == num2) ++p2;             if(uglys[i] == num3) ++p3;             if(uglys[i] == num5) ++p5;      // 不用else,去重,2*5 跟 5*2的情况,p2与p5++         }         return uglys[n-1];     } }; ———————————————— 版权声明:本文为CSDN博主「keep_fan」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/qq_19887221/article/details/126689247 
  • Java基本查找、二分查找、插值查找、分块查找-转载
     1、基本查找方法 基本查找核心:从0索引开始挨个往后查找  需求:定义一个方法,利用基本查找,查询某个元素是否存在  代码如下  /**  * @authorDesc  * @author  * @date   * @version 1.0.0  * @description 基本查找  * 核心:从0索引开始挨个往后查找  * 需求:定义一个方法,利用基本查找,查询某个元素是否存在  */   public class BasicSearch {     public static void main(String[] args) {         int[] numArray = {1,2,3,4,5,6,4,4};         int number = 4;         System.out.println(search(numArray,number));     }     /**      * @description      * @author       * @date       * @param numArray 数组      * @param number 要查找的数字      * @retur {@link int} 返回元素下表      */     public static int search(int[]numArray,int number){         for (int i = 0; i < numArray.length; i++) {             if (numArray[i] == number){                 return i;             }         }         return -1;     }  需求:定义一个方法,利用基本查找,查询某个元素是否存在,如果有重复的元素,把所有元素找出  /**      * @description      * @author       * @date       * @param numArray 数组      * @param number 要查找的数字      * @return {@link List} 返回元素下标集合      */     public static List search(int[]numArray,int number){         List listnum = new ArrayList<>();         for (int i = 0; i < numArray.length; i++) {             if (numArray[i] == number){                 listnum.add(i);             }         }         return listnum;     } }  2、二分查找 前提条件:数组中的数据必须是有序的  核心:每次排除一半的查找范围  代码如下  /**  * @authorDesc  * @author  * @date  * @version 1.0.0  * @description 二分查找  * 核心:每次排除一半的查找范围  * 需求:定义一个方法利用二分查找,查询某个元素是否在数组中  */   public class BinarySearch {     public static void main(String[] args) {         int [] numArray = {1,2,3,4,5,6};         int number = 3;         System.out.println(search(numArray,number));     }     /**      * @description      * @author      * @date      * @param numArray 数组      * @param number 查找的数字      * @return {@link int} 返回在数组中的下标      */     public static int search(int [] numArray, int number){         //定义两个变量记录要查找的范围 min数组第一个元素的索引,max 数组最后一个元素的索引         int min = 0;         int max = numArray.length - 1;         //利用循环查找         while (true){             if (min > max){                 return -1;             }             //找到min和max的中间值             int mid = (min + max) / 2;             // 用mid 指向的元素和要查找的元素进行比较             if (numArray[mid] > number){                 //要查找的数字在mid的左边                 //min不变, max = mid - 1                 max = mid - 1;             }else if (numArray[mid] < number){                 //要查找的数字在mid的右边                 //max不变,min = mid + 1                 min = mid + 1;             }else {                 //查找的元素和mid指向得到元素一样                 return mid;             }         }     } }  二分查找的优势:提高查找效率  二份查找条件:1、数据必须是有序的  2、如果数据是乱的,先排序再用二分法查找得到的索引没有意义,只能确定当前数字在数组中是否存在,因为排序后的数字位置发生了变化  二分查找的过程:min和max表示当前要查找的范围  mid是在min和max中间的数字  如果要查找的元素在mid左边,缩小范围时,min不变,max等于mid减1  如果要查找的元素在mid右边,缩小范围时,max不变,min等于mid减1  3、插值查找 //插值查找 int mid = min + (number - numArray[min]) /(numArray[max] - numArray[min]) * (max - min); 和二分查找相似,将中间值mid改为:  int mid = min + (number - numArray[min]) /(numArray[max] - numArray[min]) * (max - min);  4、分块查找 原则:1、前一块中的最大数据,小于后一块中的所有数据(块内无序,块间有序)  2、块数数量一般等于元素个数的开根号,比如16个数字一般分为4块  核心:先确定要查找的元素在哪一块,然后在挨个查找  代码如下  /**  * @authorDesc   * @author   * @date   * @version 1.0.0  * @description 使用分块查找数组中的元素  */   public class BlockSearch {     public static void main(String[] args) {          int[] numArray = {2,3,1,4,                         8,6,7,5,                         9,11,10,12,                         15,14,13,16};  ———————————————— 版权声明:本文为CSDN博主「qi3415」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/qi341500/article/details/126688184 
  • od笔试记录-转载
    一 最小字典序 问题:给定一个字符串(仅包含小写字母),只允许交换一次两个任意位置的字符,输出这样交换的最小的字符串。 要求:仅允许一次交换 100%通过。  输入: abcdfe 输出: abcdef 输入: aaaafhd 输出: aaaadhf  思路:将字符串分为字符数组进行排序,将排序后的数组与原数组对比,出现不同时就是交换的下标,首相更新的那个钱下表为排序后当前下标的字符,关键是和那个位置的字符交换,因为是和排序后的字符比较,所以要交换的字符肯定大于原字符串中当前下标的字符,所以为了使得字符串的字典序最小(从左到右字母依次变大),要从字符串末位开始寻找(这里让我头疼了半天)。  package HuaWeiOD;  import java.util.Arrays; import java.util.Scanner;  /**  * @ClassName Problem11  * @Description TODO  * @Author shenxinyuan  * @Date 2022/8/27 $ {TIME}  * @Version 1. 0  **/ public class Problem1 {     //100%     public static void main(String[] args) {         Scanner scan = new Scanner(System.in);         String str = scan.next();         char[] chars = new char[str.length()];         char[] ret = new char[str.length()];          for (int i = 0; i < str.length(); i++) {             chars[i] = str.charAt(i);             ret[i] = str.charAt(i);         }         Arrays.sort(chars);     //最小字典序 chars          for (int i = 0; i < ret.length; i++) {             if (ret[i] != chars[i]) {                 char tmp = ret[i];                 ret[i] = chars[i];                 for(int j =ret.length-1;j>=i+1;j--){        //很关键 要倒序寻找那个字符,保证一次交换的情况下整个串的字典序是最小                     if(ret[j] == chars[i]){                         ret[j] = tmp;                         break;                     }                 }                 break;             }         }         for(int i =0;i            System.out.print(ret[i]);         }     } } 二 第k长的连续子串的长度 这个题目仅仅通过了70% 问题:给定一个字符串,字符串(仅包含大写字母)中存在有相同字符的连续字串,再输入一个数字k,输出字符串中第k长的有重复字符的字串长度。 要求:同一个字母值计算呢一个重复字串的最大长度。还有,如果k的值大于含有重复字符的字串的数量,输出-1。  输入: AAAAHHHBBCDHHHH 3 输出: 2 解释: H字符出现了两个字串,仅计算最长的长度4,长度为3的字串跳过,因此结果为BB串,长度为2  思路:用字符数组记录包含重复字符的最长字串的长度(每一个字母只能记录最长字串长度的数值),最后排序,输出对应第k大的字串长度  package HuaWeiOD;  /**  * @ClassName Problem22  * @Description TODO  * @Author shenxinyuan  * @Date 2022/8/27 $ {TIME}  * @Version 1. 0  **/  import java.util.*;  public class Problem22 {     //70%     public static void main(String[] args) {         Scanner scan = new Scanner(System.in);         int[] arr = new int[90];         String str = scan.next();         int k = scan.nextInt();          int tmp = 1;         arr[str.charAt(0)]++;         for (int i = 1; i < str.length(); i++) {             if (str.charAt(i) == str.charAt(i - 1)) {                 tmp++;             } else {                 tmp = 1;             }             //以下代码块每次循环都执行  也许是这里导致剩余的30用例没有通过             {                 if (arr[str.charAt(i)] != 0) {                     //如果是相同字母,更新长度                     arr[str.charAt(i)] = Math.max(tmp, arr[str.charAt(i)]);                 } else {                     //没有出现过最长连续字串,直接记录                     arr[str.charAt(i)] = tmp;                 }             }         }          int num = 0;    //记录字串数目         for (int i = 0; i < arr.length; i++) {             if (arr[i] > 0) {                 num++;             }         }         //升序         Arrays.sort(arr);         System.out.println(num >= k ? arr[arr.length - k] : -1);      } }   三 两个人均分积木重量 没来得及调试通过。自测符合预期。 问题:koko和它的兄弟的积木重量必须一样,否则koko会哭泣,按照koko的理解两个数字的加法为:两个数字的二进制对应相加的过程中不计权重(也就是每一位符合异或的计算方法)。例如:5 + 6 =11 koko的计算方式为 0101 + 0110 = 0011 (十进制的3,也就是koko认为5+6=3)  要求:koko认为两个人的积木重量“相同”的情况下,另一个人可以获得的最大重量  输入: 3 3 5 6 输出: 11 解释: koko认5+6“等于”3,此时koko拿3个,另一个人那5+6实际上是11个,输出结果为11  思路:首先要实现新的koko的计算方式,plus方法实现如下。然后要计算给定数组的全部子集,buildSubSet实现方法如下。在遍历全部子集比较记录koko认为重量相同的情况下:它的兄弟可获得的最大的实际重量。  package HuaWeiOD;  /**  * @ClassName Problem3  * @Description TODO  * @Author shenxinyuan  * @Date 2022/8/27 $ {TIME}  * @Version 1. 0  **/  import com.sun.org.apache.xpath.internal.operations.Plus; import org.junit.Test;  import java.lang.reflect.Array; import java.util.*;  import static HuaWeiOD.ZiJi.buildSubSet;  public class Problem3 {     /* 测试用例:     输入:     3     3 5 6     输出:     11     5 + 6 = 11     但是koko认为 5+6=3  认为两个人的重量“相同”的情况下11最大     */     //相当于按照新的加法运算方式,计算最大的分段和     public static void main(String args[]) {         Scanner scan = new Scanner(System.in);         int n = scan.nextInt();          int[] arr = new int[n];         for(int i =0;i            arr[i] = scan.nextInt();         }         int newSuma = 0;         int newSumb = 0;         int max =0;         int suma = 0;         int sumb = 0;         //全集         List> res = buildSubSet(arr);         for(int i =0;i            List tmp = res.get(i);             for(int j =0;j                if(tmp.contains(arr[j])){                     newSuma = plus(newSuma,arr[j]);                     suma += arr[j];                 }else {                     newSumb = plus(newSumb,arr[j]);                     sumb += arr[j];                 }             }             if(newSuma!=0&&newSuma==newSumb){                 max = Math.max(Math.max(suma,sumb),max);                 System.out.println("max = " + max);                 System.out.println("newSuma = " + newSuma);                 System.out.println("----------------");             }             newSuma = 0;             newSumb = 0;             suma =0;             sumb=0;         }         System.out.println(max);     }      //自测成功     @Test     public void test(){         System.out.println("plus(5,6) = " + plus(5, 6)); //        System.out.println("plus(3,5) = " + plus(3, 5));     }      //按照koko的加法运算方式(即二进制加法情况加不做加权,新的二进制位是连个二进制位的异或)     public static int plus(int a, int b) {         StringBuilder tmp = new StringBuilder();         String stra = Integer.toBinaryString(a);         String strb = Integer.toBinaryString(b);         //记录最小的二进制位长度         int length = Math.min(stra.length(), strb.length());         for (int i = 0; i < length; i++) {             //新的二进制位是连个二进制位的异或             tmp.insert(0,Integer.parseInt(String.valueOf(stra.charAt(stra.length()-1-i))) ^ Integer.parseInt(String.valueOf(strb.charAt(strb.length()-1-i))));         }         //多余的二进制位头插如string         int k = stra.length() > length ? stra.length() : strb.length();         String kk = stra.length() > length ? stra : strb;         for (int i = length; i < k; i++) {             tmp.insert(0, kk.charAt(kk.length()-1-i));         }          //计算新的二进制串的十进制数值         int res = 0;         for (int i = tmp.length() - 1; i >= 0; i--) {              //方式一:自定义逻辑 二进制串转为十进制数值             int num = (int) (Integer.parseInt(String.valueOf(tmp.charAt(i))) * Math.pow(2, tmp.length() - i - 1));             res += num;              //方式二:API实现 二进制串转为十进制数值 //            res = Integer.parseInt(tmp.toString(),2);         }         return res;     }  public static List> buildSubSet(int[] nums){         List> result = new ArrayList<>();         //先添加一个空集         result.add(new ArrayList<>());         for (int i = 0;i            //获取当前子集个数             int size = result.size();             //依次取出当前子集并为每一子集添加元素nums[i]             //最后再添加回result             for (int j = 0;j                List clone = new ArrayList<>(result.get(j));                 clone.add(nums[i]);                 result.add(clone);             }         }         return result;     } } ———————————————— 版权声明:本文为CSDN博主「FHLD666」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/sxy3180911/article/details/126563776 
总条数:2301 到第 页
上滑加载中