• [技术干货] 聊聊@Autowired注解的Field injection is not recommended提示问题
    1. 前言在我接触过的大部分Java项目中,经常看到使用@Autowired注解进行字段注入:import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Servicepublic class OrderService { @Autowired private PaymentService paymentService; @Autowired private InventoryService inventoryService;}在IDEA中,以上代码@Autowired注解下会显示波浪线,鼠标悬停后提示:Field injection is not recommended,翻译过来就是不建议使用字段注入。关于该提示问题,有直接修改IDEA设置关闭该提示的,有替换为使用@Resource注解的,但这都不是该问题的本质。该问题的本质是Spring官方推荐使用构造器注入,IDEA作为一款智能化的IDE,针对该项进行了检测并给以提示。所以该提示背后的本质问题是:为什么Spring官方推荐构造器注入而不是字段注入?2. 推荐构造器注入的理由相比字段注入,构造器注入有以下几个优点:支持不可变性依赖明确单元测试友好循环依赖检测前置,提前暴露问题2.1 支持不可变性构造器注入允许将依赖字段声明为final,确保对象一旦创建,其依赖关系不再被修改。字段注入无法使用final,依赖可能在对象生命周期中被意外修改,破坏状态一致性。构造器注入示例:import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Servicepublic class OrderService { private final PaymentService paymentService; private final InventoryService inventoryService; @Autowired public OrderService(PaymentService paymentService, InventoryService inventoryService) { this.paymentService = paymentService; this.inventoryService = inventoryService; }}说明:如果Spring版本是4.3或者更高版本且只有一个构造器,构造器上的@Autowired注解可以省略。2.2 依赖明确构造器注入通过在类的构造函数中显式声明依赖,并且强制要求在创建对象时必须提供所有必须的依赖项,通过构造函数参数,使用者对该类的依赖一目了然。字段注入通过在类的字段上直接使用@Autowired注解注入依赖,依赖关系隐藏在类的内部,使用者无法直接看到该类的依赖。2.3 单元测试友好构造器注入允许直接通过new创建对象,无需依赖Spring容器或反射,降低了测试复杂度。字段注入需要依赖Spring容器或反射,增加了测试复杂度。2.4 循环依赖检测前置,提前暴露问题构造器注入在应用启动时直接暴露循环依赖,强制开发者通过设计解决问题。字段注入在应用启动时不会暴露循环依赖,直到实际调用时才可能暴露问题,增加调试难度。示例:假设项目中有以下两个Service存在循环依赖:import org.springframework.stereotype.Service;@Servicepublic class OrderService { private final PaymentService paymentService; public OrderService(PaymentService paymentService) { this.paymentService = paymentService; }}import org.springframework.stereotype.Service;@Servicepublic class PaymentService { private final OrderService orderService; public PaymentService(OrderService orderService) { this.orderService = orderService; }}此时启动项目会报错,抛出org.springframework.beans.factory.BeanCurrentlyInCreationException异常,大致的异常信息如下所示:Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'orderService': Requested bean is currently in creation: Is there an unresolvable circular reference?将以上两个Service修改为字段注入:import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Servicepublic class OrderService { @Autowired private PaymentService paymentService;}import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Servicepublic class PaymentService { @Autowired private OrderService orderService;}此时启动项目不会报错,可以启动成功。3. @RequiredArgsConstructor注解的使用及原理为了避免样板化代码或者为了简化代码,有的项目中可能会使用@RequiredArgsConstructor注解来代替显式的构造方法:import lombok.RequiredArgsConstructor;import org.springframework.stereotype.Service;@RequiredArgsConstructor@Servicepublic class OrderService { private final PaymentService paymentService; private final InventoryService inventoryService;}接下来简单讲解下@RequiredArgsConstructor注解的原理。@RequiredArgsConstructor注解用于在编译时自动生成包含特定字段的构造方法。字段筛选逻辑如下所示:被final修饰的未显式初始化的非静态字段被@NonNull注解标记的未显式初始化的非静态字段示例:import lombok.NonNull;import lombok.RequiredArgsConstructor;@RequiredArgsConstructorpublic class User { private final String name; @NonNull private Integer age; private final String address = ""; private String email; private static String city; @NonNull private String sex = "男";}以上代码在编译时自动生成的构造方法如下所示:public User(String name, @NonNull Integer age) { if (age == null) { throw new NullPointerException("age is marked non-null but is null"); } else { this.name = name; this.age = age; }}从生成的构造方法可以看出:1)如果字段被lombok.NonNull注解标记,在生成的构造方法内会做null值检查。2)address字段虽然被final修饰,但因为已初始化,所以未包含在构造方法中。3)email字段既没被final修饰,也没被lombok.NonNull注解标记,所以未包含在构造方法中。4)city字段是静态字段,所以未包含在构造方法中。5)sex字段虽然被lombok.NonNull注解标记,但因为已初始化,所以未包含在构造方法中。4. 总结@Autowired注解在IDEA中提示:Field injection is not recommended,其背后的本质问题是:Spring官方推荐构造器注入而不是字段注入。而Spring官方推荐构造器注入,是因为相比字段注入,构造器注入有以下几个优点:支持不可变性依赖明确单元测试友好循环依赖检测前置,提前暴露问题使用构造器注入时,为了避免样板化代码或者为了简化代码,可以使用@RequiredArgsConstructor注解来代替显式的构造方法,因为@RequiredArgsConstructor注解可以在编译时自动生成包含特定字段的构造方法。至于项目中要不要使用构造器注入,使用显式的构造方法还是使用@RequiredArgsConstructor注解来简化代码,可以根据个人喜好及团队规范自行决定。转载自https://www.cnblogs.com/zwwhnly/p/18907966
  • [技术干货] 使用Java Stream,将集合转换为一对一Map
    在日常的开发工作中,我们经常使用到Java Stream,特别是Stream API中提供的Collectors.toList()收集器,但有些场景下,我们需要将集合转换为Map,这时候就需要使用到Stream API中提供的另一个收集器:Collectors.toMap,它可以将流中的元素映射为键值对,并收集到一个Map中。1. 三种主要的重载方法Collectors.toMap有3种重载方法,分别是:1)两个参数的重载方法(最简单的形式)public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper) { return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);}2)三个参数的重载方法(包含冲突处理)public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction) { return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);}3)四个参数的重载方法(指定Map实现)public static <T, K, U, M extends Map<K, U>> Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction, Supplier<M> mapSupplier) { BiConsumer<M, T> accumulator = (map, element) -> map.merge(keyMapper.apply(element), valueMapper.apply(element), mergeFunction); return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID);}接下来,我们结合使用示例详细讲解。2. 使用示例2.1 将对象的某些属性转换为Map假设有一个城市列表,需要将其转换为Map,其中Key为城市ID、Value为城市名称,转换方法如下所示:@Getter@Setterpublic class City { private Integer cityId; private String cityName; public City(Integer cityId, String cityName) { this.cityId = cityId; this.cityName = cityName; }}List<City> cityList = Arrays.asList( new City(1, "北京"), new City(2, "上海"), new City(3, "广州"), new City(4, "深圳"));Map<Integer, String> cityMap = cityList.stream() .collect(Collectors.toMap(City::getCityId, City::getCityName));System.out.println(cityMap);输出结果: 2.2 将对象列表转换为Map(ID -> 对象)仍然使用上面的城市列表,需要将其转换为Map,其中Key为城市ID、Value为城市对象,转换方法如下所示:List<City> cityList = Arrays.asList( new City(1, "北京"), new City(2, "上海"), new City(3, "广州"), new City(4, "深圳"));Map<Integer, City> cityMap = cityList.stream() .collect(Collectors.toMap(City::getCityId, city -> city));City city = cityMap.get(1);System.out.println("城市ID: " + city.getCityId());System.out.println("城市名称: " + city.getCityName());输出结果如下所示:城市ID: 1城市名称: 北京上面的写法等价于:Map<Integer, City> cityMap = cityList.stream() .collect(Collectors.toMap(City::getCityId, Function.identity()));因为Function.identity()内部实现是下面这样的:static <T> Function<T, T> identity() { return t -> t;}2.3 键冲突处理假设上面的城市列表中有一个ID重复的城市:List<City> cityList = Arrays.asList( new City(1, "北京"), new City(2, "上海"), new City(3, "广州"), new City(4, "深圳"), new City(4, "天津"));Map<Integer, String> cityMap = cityList.stream() .collect(Collectors.toMap(City::getCityId, City::getCityName));System.out.println("城市ID: 4, 城市名称: " + cityMap.get(4));此时运行代码,会抛出java.lang.IllegalStateException异常,如下图所示:有3种常见的键冲突处理方式,分别是保留旧值、使用新值和合并值,接下来一一讲解。1)方式一:保留旧值Map<Integer, String> cityMap = cityList.stream() .collect(Collectors.toMap(City::getCityId, City::getCityName, (oldValue, newValue) -> oldValue));输出结果:城市ID: 4, 城市名称: 深圳2)方式二:使用新值Map<Integer, String> cityMap = cityList.stream() .collect(Collectors.toMap(City::getCityId, City::getCityName, (oldValue, newValue) -> newValue));输出结果:城市ID: 4, 城市名称: 天津3)方式三:合并值Map<Integer, String> cityMap = cityList.stream() .collect(Collectors.toMap(City::getCityId, City::getCityName, (oldValue, newValue) -> oldValue + ", " + newValue));输出结果:城市ID: 4, 城市名称: 深圳, 天津2.4 数据分组聚合假设有一个销售记录列表,需要将其转换为Map,其中Key为销售员、Value为该销售员的总销售额,转换方法如下所示:@Getter@Setterpublic class SalesRecord { private String salesPerson; private BigDecimal amount; public SalesRecord(String salesPerson, BigDecimal amount) { this.salesPerson = salesPerson; this.amount = amount; }}List<SalesRecord> salesRecordList = Arrays.asList( new SalesRecord("张三", new BigDecimal("1000")), new SalesRecord("李四", new BigDecimal("2000")), new SalesRecord("张三", new BigDecimal("980")));Map<String, BigDecimal> salesRecordMap = salesRecordList.stream() .collect(Collectors.toMap(SalesRecord::getSalesPerson, SalesRecord::getAmount, BigDecimal::add));System.out.println(salesRecordMap);输出结果: 上面的例子是销售额累加,也可以只取最小值:Map<String, BigDecimal> salesRecordMap = salesRecordList.stream() .collect(Collectors.toMap(SalesRecord::getSalesPerson, SalesRecord::getAmount, BigDecimal::min));此时的输出结果: 或者只取最大值:Map<String, BigDecimal> salesRecordMap = salesRecordList.stream() .collect(Collectors.toMap(SalesRecord::getSalesPerson, SalesRecord::getAmount, BigDecimal::max));此时的输出结果: 2.5 指定Map实现默认情况下,Collectors.toMap是将结果收集到HashMap中,如果有需要,我们也可以指定成TreeMap或者LinkedHashMap。如果想要保持插入顺序,可以指定使用LinkedHashMap:List<City> cityList = Arrays.asList( new City(2, "上海"), new City(1, "北京"), new City(4, "深圳"), new City(3, "广州"));Map<Integer, String> cityMap = cityList.stream() .collect(Collectors.toMap(City::getCityId, City::getCityName, (existing, replacement) -> existing, LinkedHashMap::new));System.out.println(cityMap);输出结果: 如果想要按键排序,可以指定使用TreeMap:List<City> cityList = Arrays.asList( new City(2, "上海"), new City(1, "北京"), new City(4, "深圳"), new City(3, "广州"));Map<Integer, String> cityMap = cityList.stream() .collect(Collectors.toMap(City::getCityId, City::getCityName, (existing, replacement) -> existing, TreeMap::new));System.out.println(cityMap);输出结果: 3. 注意事项3.1 空异常如果valueMapper中取出的值有null值,会抛出java.lang.NullPointerException异常,如下示例:List<City> cityList = Arrays.asList( new City(1, "北京"), new City(2, "上海"), new City(3, "广州"), new City(4, "深圳"), new City(5, null));Map<Integer, String> cityMap = cityList.stream() .collect(Collectors.toMap(City::getCityId, City::getCityName));System.out.println(cityMap);运行以上代码会抛出异常,如下图所示:有两种解决方案,第一种解决方案是过滤null值:Map<Integer, String> cityMap = cityList.stream() .filter(city -> city.getCityName() != null) .collect(Collectors.toMap(City::getCityId, City::getCityName));第二种解决方案是提供默认值:Map<Integer, String> cityMap = cityList.stream() .collect(Collectors.toMap(City::getCityId, city -> Optional.ofNullable(city.getCityName()).orElse("未知")));3.2 键重复异常如果出现重复键,且没有提供mergeFunction参数,会抛出java.lang.IllegalStateException异常,如下示例:List<City> cityList = Arrays.asList( new City(1, "北京"), new City(2, "上海"), new City(3, "广州"), new City(4, "深圳"), new City(4, "天津"));Map<Integer, String> cityMap = cityList.stream() .collect(Collectors.toMap(City::getCityId, City::getCityName));System.out.println(cityMap);运行以上代码会抛出异常,如下图所示:解决方案见本篇文章2.3 键冲突处理部分。4. 总结Collectors.toMap是Stream API中提供的一个非常方便的收集器,它可以将流中的元素映射为键值对,并收集到一个Map中。它适用于一对一映射的场景,但在使用时,要注意避免java.lang.NullPointerException异常和java.lang.IllegalStateException异常。转载自https://www.cnblogs.com/zwwhnly/p/19403765
  • [技术干货] Java 泛型擦除深度解析:原理与限制全揭秘
    Java 泛型的设计有个独特之处:类型信息只存在于编译期,运行时会被彻底擦除。这种 “擦除” 机制让很多开发者困惑:为什么List<String>和List<Integer>在运行时是同一个类型?为什么不能用基本类型作为泛型参数?为什么创建泛型数组会报错?今天我们就从泛型擦除的底层原理讲起,彻底搞懂这些问题,看清泛型的 “真面目”。一、泛型擦除:Java 泛型的 “编译期幻术”        泛型是 Java 5 引入的特性,但为了兼容之前的版本(Java 5 之前没有泛型),Java 采用了类型擦除(Type Erasure) 的实现方式:编译时检查泛型类型合法性,运行时擦除所有泛型信息。也就是说,泛型只在编译期起作用,运行时 JVM 根本不知道泛型参数的存在。1. 擦除的核心过程:从泛型到原始类型泛型擦除的本质是将泛型类型替换为其原始类型(Raw Type),具体规则:若泛型参数有上限(如<T extends Number>),则擦除为该上限类型;若泛型参数无上限(如<T>),则擦除为Object;若有多个上限(如<T extends A & B>),则擦除为第一个上限类型。示例:泛型类擦除前后对比// 泛型类定义public class Box<T extends Number> {    private T value;    public T getValue() { return value; }    public void setValue(T value) { this.value = value; }} // 擦除后(编译为字节码的实际类型)public class Box {  // 去掉泛型参数<T extends Number>    private Number value;  // T被替换为上限Number    public Number getValue() { return value; }  // 返回值类型变为Number    public void setValue(Number value) { this.value = value; }  // 参数类型变为Number}一键获取完整项目代码java2. 为什么需要擦除?—— 兼容性妥协        Java 5 之前的代码没有泛型,大量使用原始类型(如List而非List<String>)。为了让这些旧代码能与新的泛型代码无缝交互,Java 必须保证:泛型类在运行时的类型与非泛型类兼容。例如,Java 5 之前的List和 Java 5 之后的List<String>,在运行时必须是同一个类型(都是List.class),否则旧代码无法操作新的泛型集合。擦除机制正是为了实现这种兼容性。3. 擦除后的 “类型安全” 如何保证?        擦除会移除泛型信息,那运行时的类型安全怎么保证?答案是:编译器在擦除的同时,自动添加类型检查和转型代码。// 泛型代码List<String> list = new ArrayList<>();list.add("hello");String str = list.get(0); // 擦除后(编译器生成的实际代码)List list = new ArrayList();list.add("hello");  // 编译时检查:确保添加的是StringString str = (String) list.get(0);  // 自动添加转型代码一键获取完整项目代码java编译期:检查add("hello")是否符合List<String>的类型约束,若添加123会直接报错;运行期:通过自动生成的(String)转型代码,保证取出的元素类型正确(若因特殊操作导致类型不匹配,仍会抛ClassCastException)。泛型擦除原理图解二、泛型擦除带来的限制:这些操作为什么不允许?        擦除机制虽然保证了兼容性,但也给泛型带来了诸多限制。理解这些限制的根源,才能避免开发中的 “坑”。限制 1:不能用基本类型作为泛型参数        你可能注意到,List<int>会编译报错,必须用List<Integer>。这是因为:泛型擦除后会替换为 Object 或上限类型,而基本类型(int、double 等)不是 Object 的子类,无法转型。若声明List<int>,擦除后应为List<Object>,但int是基本类型,不能直接存储在Object数组中(需要装箱为 Integer);编译器为了避免这种矛盾,直接禁止基本类型作为泛型参数,强制使用包装类(Integer、Double 等)。反例(编译报错):// 错误:基本类型不能作为泛型参数List<int> intList = new ArrayList<>();  // 编译报错Map<double, boolean> map = new HashMap<>();  // 编译报错 // 正确:使用包装类List<Integer> intList = new ArrayList<>();Map<Double, Boolean> map = new HashMap<>();一键获取完整项目代码java限制 2:不能实例化泛型类型(new T())        无法在泛型类中直接创建泛型参数的实例(new T()),因为擦除后T会被替换为Object或上限类型,编译器无法确定具体类型。反例(编译报错):public class Box<T> {    public Box() {        // 错误:不能实例化泛型类型T        T value = new T();  // 编译报错    }}一键获取完整项目代码java原因:擦除后T变为Object,new T()会被视为new Object(),这显然不符合预期(我们想要的是T的实例,而非 Object)。解决方案:通过反射创建实例(需传入 Class 对象):public class Box<T> {    private T value;    // 传入Class对象,通过反射创建实例    public Box(Class<T> clazz) throws InstantiationException, IllegalAccessException {        value = clazz.newInstance();  // 合法    }} // 使用Box<String> box = new Box<>(String.class);  // 需显式传入Class对象一键获取完整项目代码java限制 3:不能创建泛型数组(new T[])        无法直接创建泛型数组(new T[10]),因为擦除后数组的实际类型是Object[],会导致类型安全问题。反例(编译报错):public class ArrayBox<T> {    public void createArray() {        // 错误:不能创建泛型数组        T[] array = new T[10];  // 编译报错    }}一键获取完整项目代码java原因:擦除后T[]变为Object[],若将其赋值给具体类型的数组(如String[]),再存入其他类型元素,会在运行时引发隐藏的ClassCastException:// 假设允许创建T[],擦除后实际为Object[]Object[] array = new Object[10];String[] strArray = (String[]) array;  // 编译不报错(危险!)strArray[0] = 123;  // 运行时抛ArrayStoreException(int不能存到String数组)一键获取完整项目代码java编译器为了避免这种隐藏的风险,直接禁止创建泛型数组。解决方案:用ArrayList<T>代替泛型数组(推荐,无需处理类型问题);创建Object[]数组,使用时手动转型(需谨慎,可能引发异常):public class ArrayBox<T> {    private Object[] array;        public ArrayBox(int size) {        array = new Object[size];  // 创建Object数组    }        public T get(int index) {        return (T) array[index];  // 取出时转型    }        public void set(int index, T value) {        array[index] = value;  // 存入时自动装箱    }}一键获取完整项目代码java限制 4:不能用instanceof判断泛型类型   instanceof是运行时类型检查,而泛型类型在运行时已被擦除,因此无法用instanceof判断泛型参数。反例(编译报错):List<String> list = new ArrayList<>();// 错误:不能用instanceof判断泛型类型if (list instanceof List<String>) {  // 编译报错    // ...}一键获取完整项目代码java原因:运行时List<String>和List<Integer>都是List类型,instanceof无法区分。替代方案:若需判断集合元素类型,可通过泛型类的Class参数(需手动传入):public class GenericChecker<T> {    private Class<T> clazz;        public GenericChecker(Class<T> clazz) {        this.clazz = clazz;    }        // 检查集合元素是否为T类型    public boolean check(List<?> list) {        for (Object obj : list) {            if (!clazz.isInstance(obj)) {                return false;            }        }        return true;    }} // 使用GenericChecker<String> checker = new GenericChecker<>(String.class);List<Object> list = Arrays.asList("a", "b", 123);System.out.println(checker.check(list));  // false(包含Integer)一键获取完整项目代码java限制 5:静态变量 / 方法不能引用泛型类的类型参数        泛型类的类型参数是实例级别的(每个实例可以有不同的类型参数),而静态成员是类级别的(所有实例共享),因此静态变量 / 方法不能使用泛型类的类型参数。反例(编译报错):原因:擦除后泛型类的类型参数消失,静态成员无法关联到具体的类型参数(不同实例的T可能不同)。注意:静态泛型方法是允许的,因为它有自己的泛型参数(独立于类的类型参数):public class StaticBox<T> {    // 正确:静态泛型方法有自己的类型参数S    public static <S> S create(S obj) {        return obj;    }}一键获取完整项目代码java泛型限制图解三、泛型擦除的 “后遗症”:桥接方法(Bridge Method)        擦除会导致一个隐藏问题:泛型类的方法重写可能在擦除后变得不兼容。为了解决这个问题,编译器会自动生成桥接方法(Bridge Method)。桥接方法的产生场景假设有泛型父类和子类:// 泛型父类class Parent<T> {    public void setValue(T value) {}} // 子类指定泛型参数为Stringclass Child extends Parent<String> {    @Override    public void setValue(String value) {}  // 重写父类方法}一键获取完整项目代码java        擦除后,父类的setValue(T)变为setValue(Object),而子类的setValue(String)与父类的setValue(Object)参数类型不同(不满足重写条件)。这会导致多态失效:Parent<String> parent = new Child();parent.setValue("hello");  // 期望调用子类的setValue(String)一键获取完整项目代码java为了保证多态正确,编译器会为子类自动生成桥接方法:class Child extends Parent {    // 编译器生成的桥接方法(重写父类的setValue(Object))    public void setValue(Object value) {        setValue((String) value);  // 调用子类实际的setValue(String)    }        // 子类自己的方法    public void setValue(String value) {}}一键获取完整项目代码java        桥接方法的作用是:在擦除后仍保持方法重写的多态性,确保父类引用调用方法时能正确指向子类实现。桥接方法验证通过反射可以看到编译器生成的桥接方法:import java.lang.reflect.Method; public class BridgeDemo {    public static void main(String[] args) {        for (Method method : Child.class.getMethods()) {            if (method.getName().equals("setValue")) {                System.out.println("方法:" + method);                System.out.println("是否桥接方法:" + method.isBridge());            }        }    }} // 输出结果:// 方法:public void Child.setValue(java.lang.String)// 是否桥接方法:false// 方法:public void Child.setValue(java.lang.Object)// 是否桥接方法:true一键获取完整项目代码java        可以清晰看到,子类有两个setValue方法,其中setValue(Object)是桥接方法(isBridge()返回 true)。四、总结:理解擦除,用好泛型        泛型擦除是 Java 为了兼容性做出的妥协,它既带来了便利(兼容旧代码),也带来了限制(类型信息丢失)。核心要点:擦除原理:编译时检查泛型类型,运行时将泛型参数替换为上限或 Object,同时自动添加类型检查和转型代码。核心限制:不能用基本类型作为泛型参数(擦除后无法兼容 Object);不能实例化泛型类型(new T())和创建泛型数组(new T[]);不能用instanceof判断泛型类型(运行时无类型信息);静态成员不能引用泛型类的类型参数(静态与实例的级别冲突)。桥接方法:编译器自动生成,用于解决擦除后方法重写的多态性问题。        理解泛型擦除,不仅能避免开发中的常见错误,更能让你明白 Java 泛型的设计哲学 —— 在兼容性和类型安全之间寻找平衡。虽然泛型有诸多限制,但合理使用(结合通配符、反射等)仍能写出灵活且安全的代码。记住:泛型是编译期的 “语法糖”,运行时它的 “真面目” 是原始类型。————————————————原文链接:https://blog.csdn.net/qq_40303030/article/details/152211655
  • [技术干货] JavaScript 时间戳转换日期格式
    在 JavaScript 中,将时间戳(通常为毫秒或秒的 Unix 时间戳)转换为日期格式是一个常见需求。可以通过原生 Date 对象、日期格式化方法或第三方库(如 Moment.js 或 date-fns)实现。以下是详细的中文讲解,介绍多种转换方法,包含代码示例、使用场景和注意事项。1. 时间戳简介定义:时间戳是以 1970-01-01 00:00:00 UTC(Unix 纪元)为起点的秒数或毫秒数。毫秒时间戳:如 1630454400000(毫秒,JavaScript 默认)。秒时间戳:如 1630454400(需乘以 1000 转为毫秒)。目标:将时间戳转换为可读格式,如 YYYY-MM-DD HH:mm:ss 或 2021-09-01 08:00:00。2. 转换方法方法 1:使用原生 Date 对象描述:通过 new Date(timestamp) 创建日期对象,再提取年月日等部分。适用场景:简单转换,适合不需要复杂格式化的场景。代码示例:// 毫秒时间戳const timestamp = 1630454400000; // 2021-09-01 00:00:00 UTCconst date = new Date(timestamp);// 提取日期部分const year = date.getFullYear();const month = String(date.getMonth() + 1).padStart(2, '0'); // 月份从 0 开始,需 +1const day = String(date.getDate()).padStart(2, '0');const hours = String(date.getHours()).padStart(2, '0');const minutes = String(date.getMinutes()).padStart(2, '0');const seconds = String(date.getSeconds()).padStart(2, '0');// 格式化const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;console.log(formattedDate); // 输出: 2021-09-01 00:00:00运行项目并下载源码javascript运行说明:padStart(2, '0') 确保月份、日期等为两位数。使用 UTC 方法(如 getUTCFullYear)可避免时区影响:const utcDate = `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}-${String(date.getUTCDate()).padStart(2, '0')}`;console.log(utcDate); // 输出: 2021-09-01运行项目并下载源码javascript运行方法 2:使用 toLocaleString()描述:Date 对象的 toLocaleString() 方法提供本地化格式,支持自定义选项。适用场景:需要本地化日期格式或简单格式化。代码示例:const timestamp = 1630454400000;const date = new Date(timestamp);// 默认本地化格式console.log(date.toLocaleString('zh-CN')); // 输出: 2021/9/1 08:00:00(中国时区)// 自定义格式const options = {    year: 'numeric',    month: '2-digit',    day: '2-digit',    hour: '2-digit',    minute: '2-digit',    second: '2-digit',    hour12: false};console.log(date.toLocaleString('zh-CN', options)); // 输出: 2021-09-01 08:00:00运行项目并下载源码javascript运行说明:toLocaleString 根据地区(zh-CN、en-US)调整格式。options 参数支持灵活定制。注意:格式因浏览器和地区不同而异。方法 3:自定义格式化函数描述:编写函数根据指定格式(如 YYYY-MM-DD)转换时间戳。适用场景:需要统一、可控的日期格式。代码示例:function formatDate(timestamp, format = 'YYYY-MM-DD HH:mm:ss') {    const date = new Date(timestamp);    const map = {        'YYYY': date.getFullYear(),        'MM': String(date.getMonth() + 1).padStart(2, '0'),        'DD': String(date.getDate()).padStart(2, '0'),        'HH': String(date.getHours()).padStart(2, '0'),        'mm': String(date.getMinutes()).padStart(2, '0'),        'ss': String(date.getSeconds()).padStart(2, '0')    };    return format.replace(/YYYY|MM|DD|HH|mm|ss/g, match => map[match]);}const timestamp = 1630454400000;console.log(formatDate(timestamp)); // 输出: 2021-09-01 00:00:00console.log(formatDate(timestamp, 'YYYY/MM/DD')); // 输出: 2021/09/01运行项目并下载源码javascript运行说明:支持自定义格式,灵活性高。可扩展支持更多格式(如 YYYY年MM月DD日)。方法 4:使用 Moment.js 库描述:Moment.js 是一个强大的日期处理库,支持丰富的格式化选项。适用场景:复杂日期操作或需要兼容旧项目。代码示例:// HTML: <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js"></script>const timestamp = 1630454400000;const formatted = moment(timestamp).format('YYYY-MM-DD HH:mm:ss');console.log(formatted); // 输出: 2021-09-01 00:00:00// 本地化moment.locale('zh-cn');console.log(moment(timestamp).format('LLL')); // 输出: 2021年9月1日 08:00运行项目并下载源码javascript运行说明:需要引入 Moment.js(CDN 或 NPM:npm install moment)。支持多种格式和本地化,但库体积较大(约 70KB 压缩版)。注意:Moment.js 已进入维护模式,推荐新项目使用 date-fns。方法 5:使用 date-fns 库描述:date-fns 是现代、轻量的日期处理库,模块化设计。适用场景:新项目,需轻量且现代化的日期处理。代码示例:// NPM: npm install date-fnsimport { format } from 'date-fns';const timestamp = 1630454400000;const formatted = format(new Date(timestamp), 'yyyy-MM-dd HH:mm:ss');console.log(formatted); // 输出: 2021-09-01 00:00:00运行项目并下载源码javascript运行说明:按需导入,体积小(仅导入所需函数)。支持丰富的格式化选项,类似 Moment.js 但更轻量。方法 6:使用 jQuery(结合 DOM)描述:结合 jQuery 从 DOM 获取时间戳并转换。适用场景:项目已使用 jQuery,需处理用户输入的时间戳。代码示例:// HTML: <input type="text" id="timestamp" value="1630454400000"><button onclick="format()">转换</button>$(document).ready(function() {    window.format = function() {        const timestamp = $('#timestamp').val();        const date = new Date(Number(timestamp));        const formatted = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;        console.log(formatted); // 输出: 2021-09-01    };});运行项目并下载源码javascript运行说明:需引入 jQuery:<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>运行项目并下载源码html13. 综合示例以下是一个完整示例,展示多种转换方法:<!DOCTYPE html><html><head>    <title>时间戳转换</title>    <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js"></script>    <script src="https://cdn.jsdelivr.net/npm/date-fns@2.30.0/dist/date-fns.min.js"></script>    <style>        body { font-family: Arial; padding: 20px; }        input, button { margin: 10px; padding: 8px; }    </style></head><body>    <input type="text" id="timestamp" value="1630454400000" placeholder="输入时间戳">    <button onclick="convert()">转换</button>    <div id="output"></div>    <script>        function formatDate(timestamp, format = 'YYYY-MM-DD HH:mm:ss') {            const date = new Date(timestamp);            const map = {                'YYYY': date.getFullYear(),                'MM': String(date.getMonth() + 1).padStart(2, '0'),                'DD': String(date.getDate()).padStart(2, '0'),                'HH': String(date.getHours()).padStart(2, '0'),                'mm': String(date.getMinutes()).padStart(2, '0'),                'ss': String(date.getSeconds()).padStart(2, '0')            };            return format.replace(/YYYY|MM|DD|HH|mm|ss/g, match => map[match]);        }        function convert() {            const timestamp = Number(document.getElementById('timestamp').value);            if (isNaN(timestamp)) {                alert('请输入有效时间戳!');                return;            }            const date = new Date(timestamp);            const output = `                <p>原生 Date: ${formatDate(timestamp)}</p>                <p>toLocaleString: ${date.toLocaleString('zh-CN')}</p>                <p>Moment.js: ${moment(timestamp).format('YYYY-MM-DD HH:mm:ss')}</p>                <p>date-fns: ${format(new Date(timestamp), 'yyyy-MM-dd HH:mm:ss')}</p>            `;            document.getElementById('output').innerHTML = output;        }    </script></body></html>运行项目并下载源码html4. 方法对比方法    依赖    优点    缺点原生 Date    无    无依赖,简单实现    需手动格式化,代码稍长toLocaleString()    无    本地化支持,灵活选项    格式因浏览器/地区不同自定义格式化    无    完全控制格式,灵活    需编写额外代码Moment.js    Moment.js    功能强大,易用,支持本地化    体积大,维护模式date-fns    date-fns    轻量,模块化,现代    需额外引入库jQuery    jQuery    适合 jQuery 项目,简洁 DOM 操作    需引入 jQuery,增加依赖5. 注意事项时间戳单位:JavaScript Date 接受毫秒时间戳,秒时间戳需乘以 1000:const seconds = 1630454400;const date = new Date(seconds * 1000);运行项目并下载源码javascript运行12时区处理:默认使用本地时区,需用 UTC 方法(如 getUTCFullYear)处理 UTC 时间。库如 Moment.js/date-fns 支持时区插件。输入验证:检查时间戳是否有效:if (isNaN(timestamp) || timestamp < 0) {    throw new Error('无效时间戳');}运行项目并下载源码javascript运行性能:原生方法最轻量,适合简单场景。Moment.js 体积大,date-fns 更适合新项目。浏览器兼容性:Date 和 toLocaleString 广泛支持。padStart 是 ES2017,IE 不支持,需 polyfill。安全性:用户输入时间戳需验证,防止异常值或恶意输入。6. 总结首选方法:原生 Date + 自定义格式化,简单无依赖。复杂场景:使用 date-fns(轻量现代)或 Moment.js(功能全面)。jQuery 项目:结合 jQuery 处理 DOM 输入。选择依据:无依赖:原生 Date 或 toLocaleString。复杂格式:自定义函数或 date-fns。本地化:toLocaleString 或 Moment.js。测试:验证不同时间戳(毫秒/秒)、时区和格式————————————————原文链接:https://blog.csdn.net/m0_57545130/article/details/152307313
  • [技术干货] JavaScript 时间戳转换日期格式
    在 JavaScript 中,将时间戳(通常为毫秒或秒的 Unix 时间戳)转换为日期格式是一个常见需求。可以通过原生 Date 对象、日期格式化方法或第三方库(如 Moment.js 或 date-fns)实现。以下是详细的中文讲解,介绍多种转换方法,包含代码示例、使用场景和注意事项。1. 时间戳简介定义:时间戳是以 1970-01-01 00:00:00 UTC(Unix 纪元)为起点的秒数或毫秒数。毫秒时间戳:如 1630454400000(毫秒,JavaScript 默认)。秒时间戳:如 1630454400(需乘以 1000 转为毫秒)。目标:将时间戳转换为可读格式,如 YYYY-MM-DD HH:mm:ss 或 2021-09-01 08:00:00。2. 转换方法方法 1:使用原生 Date 对象描述:通过 new Date(timestamp) 创建日期对象,再提取年月日等部分。适用场景:简单转换,适合不需要复杂格式化的场景。代码示例:// 毫秒时间戳const timestamp = 1630454400000; // 2021-09-01 00:00:00 UTCconst date = new Date(timestamp);// 提取日期部分const year = date.getFullYear();const month = String(date.getMonth() + 1).padStart(2, '0'); // 月份从 0 开始,需 +1const day = String(date.getDate()).padStart(2, '0');const hours = String(date.getHours()).padStart(2, '0');const minutes = String(date.getMinutes()).padStart(2, '0');const seconds = String(date.getSeconds()).padStart(2, '0');// 格式化const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;console.log(formattedDate); // 输出: 2021-09-01 00:00:00运行项目并下载源码javascript运行说明:padStart(2, '0') 确保月份、日期等为两位数。使用 UTC 方法(如 getUTCFullYear)可避免时区影响:const utcDate = `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}-${String(date.getUTCDate()).padStart(2, '0')}`;console.log(utcDate); // 输出: 2021-09-01运行项目并下载源码javascript运行方法 2:使用 toLocaleString()描述:Date 对象的 toLocaleString() 方法提供本地化格式,支持自定义选项。适用场景:需要本地化日期格式或简单格式化。代码示例:const timestamp = 1630454400000;const date = new Date(timestamp);// 默认本地化格式console.log(date.toLocaleString('zh-CN')); // 输出: 2021/9/1 08:00:00(中国时区)// 自定义格式const options = {    year: 'numeric',    month: '2-digit',    day: '2-digit',    hour: '2-digit',    minute: '2-digit',    second: '2-digit',    hour12: false};console.log(date.toLocaleString('zh-CN', options)); // 输出: 2021-09-01 08:00:00运行项目并下载源码javascript运行说明:toLocaleString 根据地区(zh-CN、en-US)调整格式。options 参数支持灵活定制。注意:格式因浏览器和地区不同而异。方法 3:自定义格式化函数描述:编写函数根据指定格式(如 YYYY-MM-DD)转换时间戳。适用场景:需要统一、可控的日期格式。代码示例:function formatDate(timestamp, format = 'YYYY-MM-DD HH:mm:ss') {    const date = new Date(timestamp);    const map = {        'YYYY': date.getFullYear(),        'MM': String(date.getMonth() + 1).padStart(2, '0'),        'DD': String(date.getDate()).padStart(2, '0'),        'HH': String(date.getHours()).padStart(2, '0'),        'mm': String(date.getMinutes()).padStart(2, '0'),        'ss': String(date.getSeconds()).padStart(2, '0')    };    return format.replace(/YYYY|MM|DD|HH|mm|ss/g, match => map[match]);}const timestamp = 1630454400000;console.log(formatDate(timestamp)); // 输出: 2021-09-01 00:00:00console.log(formatDate(timestamp, 'YYYY/MM/DD')); // 输出: 2021/09/01运行项目并下载源码javascript运行说明:支持自定义格式,灵活性高。可扩展支持更多格式(如 YYYY年MM月DD日)。方法 4:使用 Moment.js 库描述:Moment.js 是一个强大的日期处理库,支持丰富的格式化选项。适用场景:复杂日期操作或需要兼容旧项目。代码示例:// HTML: <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js"></script>const timestamp = 1630454400000;const formatted = moment(timestamp).format('YYYY-MM-DD HH:mm:ss');console.log(formatted); // 输出: 2021-09-01 00:00:00// 本地化moment.locale('zh-cn');console.log(moment(timestamp).format('LLL')); // 输出: 2021年9月1日 08:00运行项目并下载源码javascript运行说明:需要引入 Moment.js(CDN 或 NPM:npm install moment)。支持多种格式和本地化,但库体积较大(约 70KB 压缩版)。注意:Moment.js 已进入维护模式,推荐新项目使用 date-fns。方法 5:使用 date-fns 库描述:date-fns 是现代、轻量的日期处理库,模块化设计。适用场景:新项目,需轻量且现代化的日期处理。代码示例:// NPM: npm install date-fnsimport { format } from 'date-fns';const timestamp = 1630454400000;const formatted = format(new Date(timestamp), 'yyyy-MM-dd HH:mm:ss');console.log(formatted); // 输出: 2021-09-01 00:00:00运行项目并下载源码javascript运行说明:按需导入,体积小(仅导入所需函数)。支持丰富的格式化选项,类似 Moment.js 但更轻量。方法 6:使用 jQuery(结合 DOM)描述:结合 jQuery 从 DOM 获取时间戳并转换。适用场景:项目已使用 jQuery,需处理用户输入的时间戳。代码示例:// HTML: <input type="text" id="timestamp" value="1630454400000"><button onclick="format()">转换</button>$(document).ready(function() {    window.format = function() {        const timestamp = $('#timestamp').val();        const date = new Date(Number(timestamp));        const formatted = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;        console.log(formatted); // 输出: 2021-09-01    };});运行项目并下载源码javascript运行说明:需引入 jQuery:<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>运行项目并下载源码html13. 综合示例以下是一个完整示例,展示多种转换方法:<!DOCTYPE html><html><head>    <title>时间戳转换</title>    <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js"></script>    <script src="https://cdn.jsdelivr.net/npm/date-fns@2.30.0/dist/date-fns.min.js"></script>    <style>        body { font-family: Arial; padding: 20px; }        input, button { margin: 10px; padding: 8px; }    </style></head><body>    <input type="text" id="timestamp" value="1630454400000" placeholder="输入时间戳">    <button onclick="convert()">转换</button>    <div id="output"></div>    <script>        function formatDate(timestamp, format = 'YYYY-MM-DD HH:mm:ss') {            const date = new Date(timestamp);            const map = {                'YYYY': date.getFullYear(),                'MM': String(date.getMonth() + 1).padStart(2, '0'),                'DD': String(date.getDate()).padStart(2, '0'),                'HH': String(date.getHours()).padStart(2, '0'),                'mm': String(date.getMinutes()).padStart(2, '0'),                'ss': String(date.getSeconds()).padStart(2, '0')            };            return format.replace(/YYYY|MM|DD|HH|mm|ss/g, match => map[match]);        }        function convert() {            const timestamp = Number(document.getElementById('timestamp').value);            if (isNaN(timestamp)) {                alert('请输入有效时间戳!');                return;            }            const date = new Date(timestamp);            const output = `                <p>原生 Date: ${formatDate(timestamp)}</p>                <p>toLocaleString: ${date.toLocaleString('zh-CN')}</p>                <p>Moment.js: ${moment(timestamp).format('YYYY-MM-DD HH:mm:ss')}</p>                <p>date-fns: ${format(new Date(timestamp), 'yyyy-MM-dd HH:mm:ss')}</p>            `;            document.getElementById('output').innerHTML = output;        }    </script></body></html>运行项目并下载源码html4. 方法对比方法    依赖    优点    缺点原生 Date    无    无依赖,简单实现    需手动格式化,代码稍长toLocaleString()    无    本地化支持,灵活选项    格式因浏览器/地区不同自定义格式化    无    完全控制格式,灵活    需编写额外代码Moment.js    Moment.js    功能强大,易用,支持本地化    体积大,维护模式date-fns    date-fns    轻量,模块化,现代    需额外引入库jQuery    jQuery    适合 jQuery 项目,简洁 DOM 操作    需引入 jQuery,增加依赖5. 注意事项时间戳单位:JavaScript Date 接受毫秒时间戳,秒时间戳需乘以 1000:const seconds = 1630454400;const date = new Date(seconds * 1000);运行项目并下载源码javascript运行12时区处理:默认使用本地时区,需用 UTC 方法(如 getUTCFullYear)处理 UTC 时间。库如 Moment.js/date-fns 支持时区插件。输入验证:检查时间戳是否有效:if (isNaN(timestamp) || timestamp < 0) {    throw new Error('无效时间戳');}运行项目并下载源码javascript运行性能:原生方法最轻量,适合简单场景。Moment.js 体积大,date-fns 更适合新项目。浏览器兼容性:Date 和 toLocaleString 广泛支持。padStart 是 ES2017,IE 不支持,需 polyfill。安全性:用户输入时间戳需验证,防止异常值或恶意输入。6. 总结首选方法:原生 Date + 自定义格式化,简单无依赖。复杂场景:使用 date-fns(轻量现代)或 Moment.js(功能全面)。jQuery 项目:结合 jQuery 处理 DOM 输入。选择依据:无依赖:原生 Date 或 toLocaleString。复杂格式:自定义函数或 date-fns。本地化:toLocaleString 或 Moment.js。测试:验证不同时间戳(毫秒/秒)、时区和格式————————————————原文链接:https://blog.csdn.net/m0_57545130/article/details/152307313
  • [技术干货] Java新特性-(二)Java基础语法
    一、数据类型:Java 的 “数据分类标准”1.1 数据类型总览介绍:Java 数据类型分为基本数据类型(直接存储数据值)和引用数据类型(存储对象内存地址),两者内存存储方式不同,决定了使用场景的差异。作用:通过明确数据类型,约束变量存储的数据格式,避免数据混乱,同时优化内存占用(如用byte存储小范围整数,比int更省内存)。核心点总结:基本类型是 Java 语法的 “基础数据单元”,引用类型是基于基本类型构建的复杂数据结构;基本类型的取值范围和字节数是固定的,需根据数据大小选择合适类型(如存储 “年龄” 用byte,存储 “身份证号” 用long)。1.2 基本数据类型(4 类 8 种)介绍:按功能分为整型、浮点型、字符型、布尔型,共 8 种具体类型,每种类型有固定的字节数和取值范围。作用:满足不同场景下的数据存储需求(如整数用整型,小数用浮点型,逻辑判断用布尔型)。使用方式代码与详情:  public static void main(String[] args) {        // 1. 整型(存储整数)        byte age = 25; // 1字节,范围:-128~127(适合小范围整数)        short height = 175; // 2字节,范围:-32768~32767        int score = 95; // 4字节,范围:-2³¹~2³¹-1(整数默认类型)        long id = 1234567890123L; // 8字节,范围:-2⁶³~2⁶³-1(需加L后缀)        // 2. 浮点型(存储小数)        float weight = 62.5f; // 4字节,单精度(需加f后缀)        double pi = 3.1415926; // 8字节,双精度(小数默认类型,精度更高)        // 3. 字符型(存储单个字符)        char gender = '男'; // 2字节,范围:0~65535(支持中文)        char letter = 'A'; // 对应ASCII码65        // 4. 布尔型(存储逻辑值)        boolean isPass = true; // 1字节,仅true/false两个值(用于判断)        // 输出验证        System.out.println("年龄:" + age);        System.out.println("圆周率:" + pi);        System.out.println("是否及格:" + isPass);    }}一键获取完整项目代码java123456789101112131415161718192021222324整型中int是默认类型,long需加L后缀(小写l易与1混淆,不推荐);浮点型中double是默认类型,float需加f后缀;char类型本质是数值类型,可直接参与运算(如’A’ + 1结果为 66,对应’B’);boolean类型不能用 0/1 代替false/true,仅能存储逻辑值。1.3 引用数据类型(以 String 为例)介绍:String 是 Java 专门处理字符串的引用类型,字符串是 “多个字符的组合”,需用双引号包裹(如"Hello")。作用:存储文本信息(如用户名、地址),提供丰富的字符串处理方法(如截取、替换、判断相等)。使用方式代码:  public static void main(String[] args) {        // 方式1:简化写法(推荐),字符串常量池存储(相同内容仅存一份,省内存)        String name1 = "Java编程";        String name2 = "Java编程";    }一键获取完整项目代码java12345二、类型转换:解决 “数据类型不匹配” 问题2.1 自动类型转换(隐式转换)介绍:当 “小范围类型数据” 赋值给 “大范围类型变量” 时,Java 自动完成转换,无需手动干预(如byte→int)。作用:避免简单赋值场景的语法错误,简化代码编写(如无需手动转换byte到int)。使用方式代码:public static void main(String[] args) {    byte a = 10; // 小范围(1字节)    int b = a; // 自动转换:byte→int(4字节),无需手动处理    double c = b; // 自动转换:int→double(8字节)    System.out.println("b = " + b); // 10    System.out.println("c = " + c); // 10.0    // char与int的自动转换(char范围0~65535,int范围更大)    char ch = 'A';    int chValue = ch; // 自动转换:char→int,值为65    System.out.println("'A'对应的ASCII码:" + chValue);}一键获取完整项目代码java12345678910111213自动转换的前提是 “小范围→大范围”,常见顺序:byte→short→int→long→float→double、char→int;char类型可自动转换为int(因char的取值范围在int的正区间内),但byte不能自动转换为char(byte有负值)。2.2 表达式中的自动类型提升介绍:在算术表达式中,所有变量会先提升到 “表达式中的最高类型”,再参与运算,结果类型与最高类型一致。作用:避免运算过程中的数据溢出,保证运算精度(如byte参与运算时先转int,避免字节级运算溢出)。使用方式代码:  public static void main(String[] args) {        // 示例1:byte+short→int        byte a = 5;        short b = 10;        // int c = a + b; // 正确:a和b先转int,结果为int        // byte d = a + b; // 错误:结果是int,不能直接赋值给byte        // 示例2:int+double→double        int num1 = 3;        double num2 = 2.5;        double result = num1 + num2; // 正确:num1先转double,结果为double        System.out.println("3 + 2.5 = " + result); // 5.5        // 示例3:char+int→int        char ch = 'a'; // ASCII码97        int sum = ch + 3; // ch先转int,结果为100(对应'd')        System.out.println("'a' + 3 = " + sum + "(对应字符'd')");    }一键获取完整项目代码java123456789101112131415161718表达式提升的关键规则:byte/short/char参与运算时,先自动转为int;其他类型按 “范围从低到高” 提升到最高类型;若需将表达式结果赋值给小范围类型,需手动强制转换(如byte d = (byte)(a + b)),但需注意数据溢出风险。2.3 强制类型转换(显式转换)介绍:当 “大范围类型数据” 赋值给 “小范围类型变量” 时,需手动指定目标类型(如double→int),可能导致精度损失或数据溢出。作用:在明确数据范围可控的场景下,实现 “大范围→小范围” 的转换(如将double小数转为int整数)。使用方式代码:   public static void main(String[] args) {        // 示例1:double→int(精度损失)        double price = 98.9;        int pay = (int) price; // 强制转换:丢弃小数部分,结果为98        System.out.println("支付金额(整数):" + pay);        // 示例2:int→byte(数据溢出风险)        int num1 = 150; // int范围:-2147483648~2147483647        byte num2 = (byte) num1; // byte范围:-128~127,150超出范围,结果为-106(溢出)        System.out.println("int 150强制转为byte:" + num2);        // 示例3:表达式结果强制转换        int a = 10;        int b = 3;        double avg1 = a / b; // 3.0(int/int=int,再自动转double)        double avg2 = (double)a / b; // 3.333...(a先转double,结果为double)        System.out.println("10/3(int运算):" + avg1);        System.out.println("10/3(强制转double):" + avg2);    }一键获取完整项目代码java强制转换语法:目标类型 变量名 = (目标类型) 待转换数据;,括号不可省略;风险提示:浮点型转整型会 “直接丢弃小数”(非四舍五入),若源数据超出目标类型范围,会导致数据溢出(结果为异常值),需谨慎使用;优化建议:转换前可先判断数据范围(如if (num1 <= 127 && num1 >= -128)),避免溢出。三、运算符:Java 的 “计算与判断工具”3.1 算术运算符介绍:用于实现基本的算术运算(加、减、乘、除、取余),其中+还可用于字符串连接。作用:处理数值计算(如求总和、平均值)和字符串拼接(如拼接用户信息)。使用方式代码: public static void main(String[] args) {        // 1. 基本算术运算        int a = 10;        int b = 3;        System.out.println("a + b = " + (a + b)); // 13        System.out.println("a - b = " + (a - b)); // 7        System.out.println("a * b = " + (a * b)); // 30        System.out.println("a / b = " + (a / b)); // 3(整数相除,丢弃小数)        System.out.println("a % b = " + (a % b)); // 1(取余,结果符号与被除数一致)        // 2. 字符串连接(+两边有字符串则为连接符)        String name = "张三";        int age = 20;        System.out.println("姓名:" + name + ",年龄:" + age); // 姓名:张三,年龄:20        // 3. 数值拆分(取余+除法)        int num = 789;        int ge = num % 10; // 个位:9        int shi = num / 10 % 10; // 十位:8        int bai = num / 100; // 百位:7        System.out.println("三位数" + num + "拆分:百位" + bai + ",十位" + shi + ",个位" + ge);    }一键获取完整项目代码java整数相除(/)结果为整数,若需小数结果,需将其中一个操作数转为浮点型(如(double)a / b);取余(%)的结果符号与 “被除数” 一致(如-10 % 3 = -1,10 % -3 = 1);+的双重作用:两边都是数值则为加法,有一个是字符串则为连接(如1 + “2” = “12”,而非3)。3.2 自增自减运算符(++/–)介绍:用于对变量值 “加 1”(++)或 “减 1”(–),分为 “前缀”(先变后用)和 “后缀”(先用后变)两种用法。作用:简化变量自增 / 自减的代码(如i = i + 1可简写为i++),常用于循环变量更新。使用方式代码: public static void main(String[] args) {        // 1. 后缀自增(先用后变)        int num1 = 5;        System.out.println("num1++ = " + num1++); // 5(先输出5,再num1=6)        System.out.println("num1 = " + num1); // 6        // 2. 前缀自增(先变后用)        int num2 = 5;        System.out.println("++num2 = " + ++num2); // 6(先num2=6,再输出6)        System.out.println("num2 = " + num2); // 6        // 3. 自减示例(与自增逻辑一致)        int num3 = 5;        System.out.println("num3-- = " + num3--); // 5(先用后变,num3=4)        System.out.println("--num3 = " + --num3); // 3(先变后用,num3=3)    }一键获取完整项目代码java核心区别:后缀(变量++)是 “先使用变量当前值,再更新变量”;前缀(++变量)是 “先更新变量,再使用变量新值”;注意事项:++/–只能用于变量(如5++错误),且不建议在复杂表达式中使用(如a = b++ + ++b,结果易混淆)。3.3 赋值运算符介绍:用于给变量赋值,分为 “基本赋值”(=)和 “复合赋值”(+=、-=等),复合赋值会自动进行强制转换。作用:简化变量更新的代码(如a = a + 3可简写为a += 3),同时避免类型转换错误。使用方式代码:public static void main(String[] args) {        // 1. 基本赋值(=)        int x = 10;        System.out.println("x = " + x); // 10        // 2. 复合赋值(自动强制转换)        byte y = 5;        y += 3; // 等价于 y = (byte)(y + 3),自动强制转换,避免错误        // y = y + 3; // 错误:y+3是int,不能直接赋值给byte        System.out.println("y += 3 后:" + y); // 8        int z = 20;        z -= 5; // 等价于 z = z - 5 → 15        z *= 2; // 等价于 z = z * 2 → 30        z /= 4; // 等价于 z = z / 4 → 7(整数相除)        z %= 3; // 等价于 z = z % 3 → 1        System.out.println("z经过一系列操作后:" + z); // 1    }一键获取完整项目代码java复合赋值运算符(+=、-=、*=、/=、%=)的优势:自动对结果进行强制转换,适合小范围类型变量更新;基本赋值(=)的右结合性:如a = b = c = 5,从右到左执行,最终a、b、c均为 5。3.4 关系运算符介绍:用于判断两个数据的关系(大小、相等),结果仅为true(成立)或false(不成立),常用于分支和循环的条件判断。作用:构建条件表达式(如score >= 60判断是否及格),控制程序流程。使用方式代码:public static void main(String[] args) {        int score = 85;        int passLine = 60;        // 关系运算结果为boolean        System.out.println("score > passLine:" + (score > passLine)); // true        System.out.println("score >= 90:" + (score >= 90)); // false        System.out.println("score == passLine:" + (score == passLine)); // false        System.out.println("score != passLine:" + (score != passLine)); // true        // 结合分支使用        if (score >= passLine) {            System.out.println("考试及格");        } else {            System.out.println("考试不及格");        }        // 注意:判断相等用==,不能用=(=是赋值)        int a = 5;        int b = 5;        // if (a = b) { ... } // 错误:a = b是赋值,结果为5(int),不能作为条件        if (a == b) {            System.out.println("a和b相等");        }    }一键获取完整项目代码java关系运算符的结果是boolean类型,只能用于条件判断(如if、while的条件);易错点:判断 “相等” 用==,单个=是赋值运算符(如if (a = b)是语法错误,因赋值结果是数值,非boolean);描述区间需用&&连接两个关系表达式(如score >= 60 && score <= 100,不能写60 <= score <= 100)。3.5 逻辑运算符(处理 boolean 值)介绍:用于对boolean类型的结果进行逻辑运算(与、或、非、异或),其中&&和||具有 “短路特性”。作用:组合多个条件表达式(如 “年龄≥18 且身份证有效”),实现复杂逻辑判断。使用方式代码:public static void main(String[] args) {int age = 20;boolean hasId = true;  // 1. 逻辑与(&&):两边都为true,结果才为true(短路:左边false则右边不执行)    boolean canVote = age >= 18 && hasId;    System.out.println("是否能投票:" + canVote); // true    // 2. 逻辑或(||):两边有一个为true,结果就为true(短路:左边true则右边不执行)    boolean isAdult = age >= 18 || hasId;    System.out.println("是否成年:" + isAdult); // true    // 3. 逻辑非(!):取反(true→false,false→true)    boolean isMinor = !isAdult;    System.out.println("是否未成年:" + isMinor); // false    // 4. 短路特性演示    int num = 5;    // &&短路:左边false,右边num++不执行,num仍为5    boolean flag1 = (num > 10) && (num++ > 0);    System.out.println("flag1 = " + flag1 + ",num = " + num); // false,5    // ||短路:左边true,右边num++不执行,num仍为5    boolean flag2 = (num < 10) || (num++ > 0);    System.out.println("flag2 = " + flag2 + ",num = " + num); // true,5}一键获取完整项目代码java短路特性是&&和||的关键:&&左边为false时,右边表达式不执行;||左边为true时,右边表达式不执行(可提高效率,避免无效运算);逻辑非(!)是单目运算符,仅需一个操作数(如!hasId);逻辑异或(^)较少用,规则是 “两边不同为true,相同为false”。3.6 三元运算符介绍:由 “条件表达式” 和 “两个结果值” 组成,是简化if-else的语法,格式为条件 ? 值1 : 值2。作用:在 “二选一” 场景下简化代码(如 “判断及格与否,返回对应评语”),比if-else更简洁。使用方式代码: public static void main(String[] args) {        // 示例1:判断成绩是否及格,返回评语        int score = 75;        String result = score >= 60 ? "及格" : "不及格";        System.out.println("成绩评定:" + result); // 及格        // 示例2:求两个数的最大值        int num1 = 20;        int num2 = 35;        int max = num1 > num2 ? num1 : num2;        System.out.println(num1 + "和" + num2 + "的最大值:" + max); // 35        // 示例3:嵌套使用(不推荐,可读性低,复杂场景用if-else)        int score2 = 92;        String grade = score2 >= 90 ? "优秀" : (score2 >= 80 ? "良好" : "及格");        System.out.println("成绩等级:" + grade); // 优秀    }一键获取完整项目代码java执行逻辑:先判断条件,条件为true返回值1,为false返回值2;语法要求:值1和值2必须为同一类型(或可自动转换为同一类型),如score >= 60 ? “及格” : 0是错误的(字符串与整数类型不匹配);使用建议:简单二选一场景用三元运算符,复杂逻辑(如嵌套超过 2 层)用if-else,保证代码可读性。四、键盘录入:实现 “用户交互”4.1 Scanner 类的作用与使用步骤介绍:java.util.Scanner是 Java 提供的键盘录入工具类,用于获取用户从键盘输入的字符串、整数、小数等数据。作用:实现程序与用户的交互(如让用户输入用户名、年龄),为动态数据处理提供输入支持。使用方式代码(完整步骤):// 步骤1:导入Scanner类(JDK11+可省略,IDEA自动导入)import java.util.Scanner;public class ScannerDemo {    public static void main(String[] args) {        // 步骤2:创建Scanner对象(System.in表示从键盘输入)        Scanner sc = new Scanner(System.in);        // 步骤3:提示用户输入(提升用户体验,可选)        System.out.print("请输入您的姓名:");        // 步骤4:获取输入的字符串(next():获取空格前的内容)        String name = sc.next();        System.out.print("请输入您的年龄:");        // 获取输入的整数        int age = sc.nextInt();        System.out.print("请输入您的身高(米):");        // 获取输入的小数        double height = sc.nextDouble();        // 步骤5:使用输入的数据        System.out.println("\n=== 用户信息 ===");        System.out.println("姓名:" + name);        System.out.println("年龄:" + age + "岁");        System.out.println("身高:" + height + "米");        // 步骤6:关闭Scanner(释放资源,可选但推荐)        sc.close();    }}一键获取完整项目代码java核心步骤:导包→创建对象→获取输入→使用数据→关闭对象;常用获取方法:next()(字符串,空格截断)、nextInt()(整数)、nextDouble()(小数)、nextLine()(整行字符串,含空格);注意事项:nextInt()/nextDouble()后若直接用nextLine(),会读取到 “换行符”,需先调用sc.nextLine()清空换行符(如sc.nextInt(); sc.nextLine(); String addr = sc.nextLine();)。————————————————原文链接:https://blog.csdn.net/kong7906928/article/details/156065332
  • [技术干货] java--继承篇
    目录1.继承2.子类访问父类的成员变量3.子类访问父类的成员方法4.super关键字5.初始化顺序6.final关键字引言面向对象三大特性:封装,继承和多态。今天我们就来聊聊继承。1.继承1.1为什么需要继承因为我们在创建类时可能会大量用到重复的字段和方法,为了解决这个问题,面向对象思想中提出了继承的概念,专门用来进行共性抽取,实现代码复用。1.2继承的语法继承需要使用到extends关键字修饰符 class 子类 extends 父类{// 主体}一键获取完整项目代码java1232.子类访问父类的成员变量子类继承了父类的方法和字段,子类在方法中就可以直接访问父类的成员吗?2.1子类和父类不存在同名成员变量class Base {    public int a = 1;    public int b = 2;}class Derievd extends Base{    public int c = 3;    public void func() {        System.out.println(this.a);// 访问从父类中继承下来的a        System.out.println(this.b);// 访问从父类中继承下来的b    }}public class Test {    public static void main(String[] args) {        Derievd derievd = new Derievd();        derievd.func();    }}一键获取完整项目代码java运行结果:2.2子类和父类成员变量同名代码如下:class Base {    public int a = 1;    public int b = 2;}class Derievd extends Base{    public int c = 3;    public int a = 100;    public void func() {        System.out.println(this.a);//访问子类自己新增加的a        System.out.println(this.b);//访问从父类中继承下来的b    }}public class Test {    public static void main(String[] args) {        Derievd derievd = new Derievd();        derievd.func();    }}一键获取完整项目代码java运行结果:得出结论:当子类和父类成员同名时,优先访问子类自己的成员。特性:遵循就近原则,子类有的优先使用自己的,没有再去父类中找。2.3如果一定要访问父类的成员怎么做?方法1:使用super关键字方法2:初始化一个父类对象,用父类对象的引用访问父类的成员变量。class Base {    public int a = 1;    public int b = 2;}class Derievd extends Base{    public int c = 3;    public int a = 100;    //子类当中 如何访问父类的成员    public void func() {        //方法1        System.out.println("父类的a: "+super.a);        System.out.println(this.a);        //方法2        Base base = new Base();        System.out.println(base.a);    }}public class Test {    public static void main(String[] args) {        Derievd derievd = new Derievd();        derievd.func();    }}一键获取完整项目代码java执行结果:3子类中访问父类的成员方法3.1成员方法名字不同class Base {    public void methodA(){        System.out.println("Base中的methodA()");    }}class Derived extends Base{        public void methodB(){        System.out.println("Derived中的methodB()方法");    }    public void func(){        this.methodB();  // 访问子类自己的methodB()        this.methodA();  // 访问父类继承的methodA()    }}public class Test {    public static void main(String[] args) {        Derived derived = new Derived();        derived.func();    }}一键获取完整项目代码java执行结果:3.2成员方法名字相同class Base {    public void methodA(){        System.out.println("Base中的methodA()");    }}class Derived extends Base{    public void methodA(){        System.out.println("Derived中的methodA()方法");    }    public void func(){        this.methodA();  // 访问子类自己的methodA()    }}public class Test {    public static void main(String[] args) {        Derived derived = new Derived();        derived.func();    }}一键获取完整项目代码java执行结果:结论:【和成员变量同名时相同】当子类和父类成员同名时,优先访问子类自己的成员。特性:遵循就近原则,子类有的优先使用自己的,没有再去父类中找。3.3如果一定要访问父类的成员怎么做?【和上面的方法相同】方法1:使用super关键字方法2:初始化一个父类对象,用父类对象的引用访问父类的成员变量。class Base {    public void methodA(){        System.out.println("Base中的methodA()");    }}class Derived extends Base{    public void methodA(){        System.out.println("Derived中的methodA()方法");    }    public void func(){        //方法1        super.methodA();  // 访问子类自己的methodA()        //方法2        Base base = new Base();        base.methodA();    }}public class Test {    public static void main(String[] args) {        Derived derived = new Derived();        derived.func();    }}一键获取完整项目代码java执行结果:4.super关键字因为子类可能会和父类成员同名,如果明确要访问父类的成员,就需要使用super关键字。public void testExtends(){    super.name = "zhangsan";//访问从父类继承下来的成员变量    super.age = 18;//访问从父类继承下来的成员变量    super.eat();///访问从父类继承下来的成员方法    color = "黄色";//访问子类自己的成员变量    run();//访问子类自己的成员方法}一键获取完整项目代码java4.1子类构造方法在继承性关系下,构造子类对象时,需要先调用基类构造方法,然后执行子类的构造方法。即先要把继承于父类的成员初始化完,才能初始化子类自己的成员。class Animal {    String name;    int age;    public Animal(String name, int age) {        this.name = name;        this.age = age;    }}public class Dog extends Animal{    String color;    public Dog(String name, int age,String color) {        super(name, age);        this.color = color;    }}一键获取完整项目代码java注意事项:若父类无显示构造方法(没写构造方法)或默认构造方法(即无参数构造方法),子类构造方法第一行默认有隐藏的super()调用,即调用子类构造方法时,先调用基类的构造方法。若父类构造方法有带参数,子类构造方法需要自己定义super()。在子类构造方法中,super()必须是第一条语句。在子类构造方法中,super()只能出现一次,并且不能和this()同时出现。第四点解释,当我们重载子类构造方法时,this()调用,会调用重载的子类构造方法。而this()和super()在构造方法中,都必须是第一条语句,所有不能同时出现。代码如下:public class Dog extends Animal{    String color;    //方法的重载    public Dog() {        this("green");//调用带有一个参数的构造方法        System.out.println("使用了不带参数的构造方法");    }    public Dog(String color) {        this.color = color;        System.out.println("使用了带有一个参数的构造方法");    }    public static void main(String[] args) {        Dog dog = new Dog();//调用无参构造方法        System.out.println(dog.color);        System.out.println("==============");        Dog dog1 = new Dog("red");//调用带有一个参数的构造方法        System.out.println(dog.color);    }}一键获取完整项目代码java运行结果: 4.2super和thissuper和tthis都可以在成员方法中用来访问:成员变量和调用其他成员方法,都可以作为构造方法的第一条语句。【相同点】1.都只能在类的非静态方法中使用,用来访问非静态成员变量和调用其他成员方法。2.在构造方法中调用时,必须是构造方法的第一条语句,并且不能同时存在。【不同点】1,this是当前对象的引用,当前对象即调用实例方法的对象,super相当于子类对象从父类继承下来的部分成员的引用。2.在非静态成员方法中,this用来访问本类的方法和属性,super用来访问从父类继承下来的方法和属性。3.在构造方法中:this()用于调用本类的构造方法,super()用于调用父类的构造方法,两者在构造方法中不能同时存在。4.在继承体系下,构造方法中一定会存在super()调用,用户没有写编译器也会自动增加,但this()用户不写则没有。5.初始化顺序5.1没有继承关系时的执行顺序我们再来回顾下初始化执行顺序,没有继承关系时的执行顺序:class Person {    public String name;    public int age;    public Person(String name, int age) {        this.name = name;        this.age = age;        System.out.println("构造方法执行");    }    {    System.out.println("实例代码块执行");     }    static {    System.out.println("静态代码块执行");     }}public class Test {    public static void main(String[] args) {        Person person1 = new Person("bit",10);        System.out.println("============================");        Person person2 = new Person("gaobo",20);    }}一键获取完整项目代码java运行结果:结论分析:1.被static修饰的静态代码块先执行,并且只执行一次,在类的加载阶段执行。2.当有对象创建时,才会执行实例代码块,执行完实例代码块,最后执行构造方法。5.1继承关系下的执行顺序class Person {    public String name;    public int age;    public Person(String name, int age) {        this.name = name;        this.age = age;        System.out.println("构造方法执行");    }    {    System.out.println("实例代码块执行");     }    static {    System.out.println("静态代码块执行");     }}class Student extends Person{    public Student(String name,int age) {    super(name,age);    System.out.println("Student:构造方法执行");     }     {    System.out.println("Student:实例代码块执行");     }     static {    System.out.println("Student:静态代码块执行");     }}public class Test {    public static void main(String[] args) {        Student student1 = new Student("张三",19);        System.out.println("===========================");        Student student2 = new Student("gaobo",20);    }}一键获取完整项目代码java执行结果:结论分析:1.父类静态代码块先执行,执行完再执行子类静态代码块。2.父类实例代码块和构造方法先执行。3.再执行子类实例代码块和构造方法。4.静态代码块只执行一次。6.final关键字final可以用来修饰变量、成员方法和类。1.被final修饰的变量或字段,表示常量,不能再被改变。final int a = 10;a = 20;  // 编译出错一键获取完整项目代码java2.被final修饰的类,不能被继承final class Person {}class Student extends Person{}一键获取完整项目代码java3.被final修饰的方法,不能重写————————————————原文链接:https://blog.csdn.net/xifeng191/article/details/155503019
  • [技术干货] Java 注解与反射实战:自定义注解从入门到精通
    前言:注解到底是什么?        你是否经常在 Java 代码中看到@Override、@Deprecated这样的标记?这些就是注解 —— 一种给代码 "贴标签" 的机制。注解本身不直接影响代码执行,但能通过工具(如编译器)或框架(如 Spring)赋予代码额外含义。        自定义注解则是让我们根据业务需求创建专属 "标签",结合反射机制能实现强大的动态逻辑(比如日志记录、权限校验、ORM 映射等)。本文将从基础到实战,带你掌握自定义注解的定义、元注解的作用,以及如何通过反射让注解 "生效"。一、自定义注解基础:@interface 关键字        自定义注解使用 @interface 关键字定义,本质上是一种特殊的接口(编译后会生成继承 java.lang.annotation.Annotation 的接口)。1.1 最简单的自定义注解// 定义一个空注解public @interface MyFirstAnnotation {}一键获取完整项目代码java这个注解没有任何属性,仅作为标记使用。可以直接标注在类、方法等元素上:@MyFirstAnnotationpublic class Demo {    @MyFirstAnnotation    public void test() {}}一键获取完整项目代码java1.2 带属性的注解注解可以包含 "属性"(类似接口的抽象方法),使用时需要为属性赋值(除非有默认值)。public @interface UserInfo {    // 字符串属性    String name();    // 整数属性,带默认值    int age() default 18;    // 数组属性    String[] hobbies() default {"coding"};}一键获取完整项目代码java使用时的语法(属性名 = 值):@UserInfo(name = "张三", age = 20, hobbies = {"篮球", "游戏"})public class Person {}一键获取完整项目代码java特殊规则:若属性名是 value,且只有这一个属性需要赋值,可省略属性名:@MyAnnotation("test")数组属性若只有一个元素,可省略大括号:hobbies = "足球"二、元注解:注解的 "注解"        元注解是用于修饰注解的注解,规定了自定义注解的使用范围、生命周期等特性。Java 内置了 4 种元注解:@Target、@Retention、@Documented、@Inherited。2.1 @Target:指定注解能修饰哪些元素   @Target 限制注解可标注的目标(如类、方法、字段等),参数是 ElementType 枚举数组,常用值:ElementType    作用范围TYPE    类、接口、枚举METHOD    方法FIELD    成员变量(包括枚举常量)PARAMETER    方法参数CONSTRUCTOR    构造方法LOCAL_VARIABLE    局部变量示例:限制注解只能用于类和方法import java.lang.annotation.Target;import java.lang.annotation.ElementType; @Target({ElementType.TYPE, ElementType.METHOD}) // 可修饰类和方法public @interface Log {}一键获取完整项目代码java如果把 @Log 标注在字段上,编译器会直接报错:public class Demo {    @Log // 编译错误:@Log不适用于字段    private String name;}一键获取完整项目代码java 图示:@Target 的作用范围限制 2.2 @Retention:指定注解的生命周期@Retention 决定注解保留到哪个阶段(源码、字节码、运行时),参数是 RetentionPolicy 枚举,必须指定:RetentionPolicy    生命周期说明    能否被反射获取SOURCE    仅存在于源码中,编译后丢弃(如@Override)    不能CLASS    保留到字节码中,但 JVM 运行时不加载(默认值)    不能RUNTIME    保留到运行时,JVM 加载,可通过反射获取    能示例:让注解在运行时可被反射获取import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) // 关键:保留到运行时public @interface Permission {    String value();}一键获取完整项目代码java为什么 RUNTIME 重要?反射是在程序运行时动态获取类信息的机制,只有 RUNTIME 级别的注解才能被反射读取,这是注解与反射结合的核心前提。图示:注解的生命周期流程 2.3 @Documented:让注解出现在 API 文档中        默认情况下,javadoc 生成的文档不会包含注解信息。@Documented 修饰的注解会被包含在文档中。示例:import java.lang.annotation.Documented; @Documented // 生成文档时包含该注解public @interface Description {    String value();} /** * 测试类 * @Description 这是一个测试类 */@Description("测试类")public class Test {}一键获取完整项目代码java生成的 javadoc 中,Test 类的文档会显示 @Description("测试类")。2.4 @Inherited:让注解可被继承@Inherited 表示注解具有继承性:如果父类被该注解标注,子类会自动继承该注解(仅对类注解有效,方法 / 字段注解不继承)。示例:import java.lang.annotation.Inherited; @Inherited // 允许继承@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)public @interface InheritedAnnotation {} // 父类标注注解@InheritedAnnotationclass Parent {} // 子类未标注,但会继承父类的@InheritedAnnotationclass Child extends Parent {}一键获取完整项目代码java通过反射验证:public class Test {    public static void main(String[] args) {        System.out.println(Child.class.isAnnotationPresent(InheritedAnnotation.class)); // 输出:true    }}一键获取完整项目代码java 图示:@Inherited 的继承效果 三、注解 + 反射:让注解 "生效"        注解本身只是标记,必须通过反射获取注解信息并执行逻辑,才能真正发挥作用。反射提供了以下核心方法(在 Class、Method、Field 等类中):方法    作用getAnnotation(Class)    获取指定类型的注解实例getAnnotations()    获取所有注解(包括继承的)isAnnotationPresent(Class)    判断是否存在指定注解实战案例:用注解实现方法权限校验需求:定义 @RequiresPermission 注解,标记方法需要的权限;通过反射调用方法前检查当前用户是否有权限,无权限则抛出异常。步骤 1:定义注解import java.lang.annotation.*; @Target(ElementType.METHOD) // 仅用于方法@Retention(RetentionPolicy.RUNTIME) // 运行时可反射获取public @interface RequiresPermission {    String[] value(); // 所需权限列表}一键获取完整项目代码java步骤 2:使用注解标注方法public class UserService {    // 需要"user:query"权限    @RequiresPermission("user:query")    public void queryUser() {        System.out.println("查询用户成功");    }     // 需要"user:add"或"admin"权限    @RequiresPermission({"user:add", "admin"})    public void addUser() {        System.out.println("新增用户成功");    }}一键获取完整项目代码java步骤 3:反射 + 注解实现权限校验import java.lang.reflect.Method;import java.util.Arrays;import java.util.HashSet;import java.util.Set; public class PermissionChecker {    // 模拟当前用户拥有的权限    private static final Set<String> CURRENT_USER_PERMISSIONS = new HashSet<>(Arrays.asList("user:query"));     // 反射调用方法并校验权限    public static void invokeWithCheck(Object obj, String methodName) throws Exception {        // 1. 获取方法对象        Method method = obj.getClass().getMethod(methodName);         // 2. 检查方法是否有@RequiresPermission注解        if (method.isAnnotationPresent(RequiresPermission.class)) {            // 3. 获取注解实例            RequiresPermission annotation = method.getAnnotation(RequiresPermission.class);            // 4. 获取注解的权限列表            String[] requiredPermissions = annotation.value();             // 5. 校验权限            boolean hasPermission = false;            for (String permission : requiredPermissions) {                if (CURRENT_USER_PERMISSIONS.contains(permission)) {                    hasPermission = true;                    break;                }            }             if (!hasPermission) {                throw new SecurityException("权限不足,需要:" + Arrays.toString(requiredPermissions));            }        }         // 6. 权限通过,调用方法        method.invoke(obj);    }     public static void main(String[] args) throws Exception {        UserService service = new UserService();        invokeWithCheck(service, "queryUser"); // 成功:查询用户成功        invokeWithCheck(service, "addUser");  // 失败:抛出SecurityException    }}一键获取完整项目代码java执行结果:查询用户成功Exception in thread "main" java.lang.SecurityException: 权限不足,需要:[user:add, admin]一键获取完整项目代码bash四、底层原理:注解本质与反射获取机制注解的本质:@interface 编译后会生成一个继承 java.lang.annotation.Annotation 的接口,例如:// 编译后自动生成的代码(简化)public interface MyAnnotation extends Annotation {    String value();    int age() default 18;}一键获取完整项目代码java注解实例的生成:当 JVM 加载被注解的类时,会通过动态代理生成注解接口的实现类实例(保存注解属性值)。反射获取注解的过程:反射通过 getAnnotation() 方法从类 / 方法的元数据中获取代理实例,从而读取属性值。五、应用场景总结注解 + 反射的组合在框架中被广泛使用:日志记录:通过注解标记需要记录日志的方法,反射拦截并打印日志(如 Spring 的@Log)。ORM 映射:用注解关联 Java 类与数据库表(如 JPA 的@Entity、@Column)。依赖注入:标记需要注入的对象(如 Spring 的@Autowired)。AOP 切面:通过注解定义切入点(如 Spring 的@Before、@After)。参数校验:验证方法参数合法性(如 Jakarta 的@NotNull、@Size)。结语        自定义注解是 Java 中 "声明式编程" 的核心体现,结合反射能极大简化代码逻辑、提高灵活性。掌握元注解的作用(尤其是@Target和@Retention)是定义有效注解的前提,而反射则是让注解从 "标记" 变为 "可执行逻辑" 的桥梁。尝试在项目中用注解解决重复逻辑(如日志、权限),你会感受到它的强大!————————————————原文链接:https://blog.csdn.net/qq_40303030/article/details/152256018
  • [技术干货] java--类和对象
    前言Java是一门面向对象的语言,在学习它之前需要先了解什么是对象?对象就是现实生活中的实体,比如,冰箱,洗衣机,人等等。 什么是面向对象?面向对象是一种解题思想,就是依靠对象之间的交互完成任务。比如,人把衣服给洗衣机,这里人和洗衣机就是对象。我们只需要使用洗衣机就可以,不需要关注洗衣机是怎么洗衣服的。这就是依靠对象之间的交互来完成任务。 类的定义和使用什么是类类是⽤来对⼀个实体(对象)来进行描述的,主要描述该实体(对象)具有哪些属性(外观尺寸等),哪些功能(用来⼲啥),描述完成后计算机就可以识别了。类可以分为两部分:一部分是字段(属性)也叫成员变量,另一部分是行为也叫成员方法。 类的定义类的定义需要用class关键字,代码如下: class ClassName{field; // 字段(属性) 或者 成员变量method; // ⾏为 或者 成员⽅法}运行项目并下载源码java运行 类的实例化什么是实例化定义了⼀个类,就相当于在计算机中定义了⼀种新的类型,与int,double类似,只不过int和double是java语言自带的内置类型,而类是用户⾃定义的类型。用类类型来创建对象的过程就是类的实例化,需要用到new关键字加上类名来实例化对象。代码如下: public class Main{public static void main(String[] args) {ClassName classname = new ClassName(); //通过new实例化对象}}运行项目并下载源码java运行 访问对象的成员比如我们定义一个Student类,代码如下: public class Student {//属性,成员变量    public String name;    public int age;        //行为  成员方法    public void eat(){        System.out.println(name+"正在吃饭.....");    }        public void sleep(){        System.out.println(name+"正在睡觉.....");    }}运行项目并下载源码java运行 这时候就可以通过对象的引用加’ . '号来访问对象当中的成员和成员方法。代码如下: public class Test {    public static void main(String[] args) {        Student student = new Student();        student.name = "zhangsan";        student.age = 18;         student.eat();        student.sleep();         System.out.println(student.name);        System.out.println(student.age);    }}运行项目并下载源码java运行 运行结果:  this关键字使用this引用的原因如下代码定义了⼀个Date类,Date类中包含3个属性分别是year,month,day。分别使⽤setDay和printDate对进行设置和打印⽇期. public class Date {public int year;public int month;public int day;public void setDay(int y, int m, int d){year = y;month = m;day = d;}public void printDate(){System.out.println(year + "/" + month + "/" + day);}public static void main(String[] args) {// 构造三个⽇期类型的对象 d1 d2 d3Date d1 = new Date();Date d2 = new Date();Date d3 = new Date();// 对d1,d2,d3的⽇期设置d1.setDay(2020,9,15);d2.setDay(2020,9,16);d3.setDay(2020,9,17);// 打印⽇期中的内容d1.printDate();d2.printDate();d3.printDate();}}运行项目并下载源码java运行 这里提出两个疑问:1.如形参名不小心与成员变量名相同,会发生什么?2.三个对象都在调⽤setDate和printDate函数,但是这两个函数中没有任何有关对象的说明,setDate和printDate函数如何知道打印的是那个对象的数据呢? public void setDay(int year, int month, int day){year = year;month = month;day = day;}运行项目并下载源码java运行 这时候运行代码就会发现,形参自己给自己赋值。那如何解决这个问题呢?这时候我们只要在成员变量前加上this.就能一劳永逸的解决这个问题。代码如下 public void setDay(int year, int month, int day){this.year = year;this.month = month;this.day = day;运行项目并下载源码java运行 this是什么this引用指向当前对象(成员方法运行时调用该成员方法的对象),在成员方法中所有成员变量的操作,都是通过该引用去访问。这就很好的回答了第二个疑问。 this的特性1.this只能在成员方法中使用2.this的类型:对应类类型引用,即哪个对象调用就是哪个对象的引用类型3.在"成员方法"中,this只能引用当前对象,不能再引用其他对象4.this是“成员方法”第⼀个隐藏的参数,编译器会自动传递,在成员方法执行时,编译器会负责将调用成员方法对象的引用传递给该成员方法,this负责来接收this隐藏参数演示如下:  对象的构造及初始化默认初始化如果没有对成员变量进行初始化,系统会给一个对应的默认值。默认值如下:  就地初始化在声明成员变量时就给出初始值 public class Student {    public String name = "zhangsan";    public int age = 18;     public void eat(){        System.out.println(name+"正在吃饭.....");    }    public void sleep(){        System.out.println(name+"正在睡觉.....");    }}运行项目并下载源码java运行 构造方法初始化构造方法概述构造方法是一种特殊的成员方法,名字要和类名必须相同,无返回值,创建对象时会由编译器自动调用。 public class Student {    public String name;    public int age;    //无参构造方法    public Student(){    System.out.println("不带参数的构造方法.....");        //this.("qiqi",18);    }    //有两个参数的构造方法    public Student(String name,int age){        this.name = name;        this.age = age;        System.out.println("调用了带有3个参数的构造方法.....");    }    public void eat(){        System.out.println(name+"正在吃饭.....");    }    public void sleep(){        System.out.println(name+"正在睡觉.....");    }        public static void main(String[] args) {        Student student = new Student("zhangsan",18);      }}运行项目并下载源码java运行 创建构造方法的快捷方法右键鼠标,点击Generate , 再点击Constructor , 然后按住Ctrl键选中所有成员变量,最后点击OK即可。 系统就会生成对应构造方法:  创建成员方法的快捷方法Getter and Setter是创建成员方法的快捷方式,也是一样选中所有成员变量,最后点击OK即可————————————————原文链接:https://blog.csdn.net/xifeng191/article/details/155202007
  • [技术干货] Java发展史及版本详细说明
    1. Java 1.0(1996年1月23日)核心功能:首个正式版本,支持面向对象编程、垃圾回收、网络编程。包含基础类库(java.lang、java.io、java.awt)。支持Applet(浏览器嵌入的小程序)。关键特性:跨平台(Write Once, Run Anywhere)。基础集合类(Vector、Hashtable)。AWT(Abstract Window Toolkit,用于GUI开发)。2. Java 1.1(1997年2月19日)核心功能:引入内部类(Inner Classes)。增强GUI支持(Swing框架的前身)。关键特性:Reflection(反射机制)。RMI(Remote Method Invocation,远程方法调用)。JDBC(数据库连接)的早期版本。3. Java 1.2(Java 2, 1998年12月8日)核心功能:正式更名为Java 2,分为三个平台:J2SE(标准版)、J2EE(企业版)、J2ME(微型版)。引入集合框架(java.util.Collections)。关键特性:Swing GUI框架(替代AWT)。校验编译器(javac改进)。严格的异常处理(必须声明或捕获检查型异常)。4. Java 1.3(2000年5月8日)核心功能:增强JIT编译器性能。引入日志框架(java.util.logging)。关键特性:HotSpot JVM(Oracle的高性能JVM)。音频API(javax.sound)。5. Java 1.4(2002年2月6日)核心功能:正则表达式(java.util.regex)。遍历器模式(Iterator)。关键特性:Assertion(断言,assert关键字)。集成Apache的XML解析库(javax.xml)。6. Java 5(Java 5.0, 2004年9月30日)核心功能:重大升级,引入多项语法和功能革新。关键特性:泛型(Generics)。枚举(Enums)。注解(Annotations,如@Override)。自动装箱/拆箱(Autoboxing/Unboxing)。增强for循环(for-each)。可变参数(Varargs)。Concurrent包(java.util.concurrent,多线程优化)。7. Java 6(2006年12月11日)核心功能:改进性能和兼容性。关键特性:Scripting API(支持JavaScript等脚本语言)。Pluggable Annotation(自定义注解处理器)。JDBC 4.0(支持类型安全查询)。Drag-and-Drop API。8. Java 7(2011年7月28日)核心功能:语法和API的进一步简化。关键特性:Try-with-resources(自动资源管理)。钻石操作符(<>推断泛型类型)。NIO 2.0(增强文件系统API,java.nio.file)。Switch支持字符串(预览功能)。Fork/Join框架(并行任务处理)。9. Java 8(2014年3月18日)核心功能:函数式编程支持,引发Java生态巨大变革。关键特性:Lambda表达式(->语法)。Stream API(集合数据处理)。默认方法(接口中的默认实现)。新的日期时间API(java.time包)。Optional类(避免空指针异常)。重复注解(@Repeatable)。10. Java 9(2017年9月21日)核心功能:模块化系统(JPMS),Java首次引入模块化。关键特性:Jigsaw项目(模块化JDK,module-info.java)。JShell(交互式命令行工具)。HTTP客户端(java.net.http)。私有接口方法(接口内部可见方法)。集合工厂方法(List.of()等不可变集合)。11. Java 10(2018年3月20日)核心功能:短期发布周期(每6个月一次)的首个版本。关键特性:局部变量类型推断(var关键字)。垃圾回收器改进(G1成为默认GC)。并行Full GC(ZGC的预览)。12. Java 11(2018年9月25日,LTS)核心功能:长期支持(LTS)版本,企业级首选。关键特性:Epsilon垃圾收集器(无操作GC,用于测试)。HTTP客户端正式版(Java 9的预览功能升级)。Unicode 8.0支持。Deprecate Nashorn引擎(JavaScript引擎)。13. Java 14(2020年3月17日)核心功能:预览特性的快速迭代。关键特性:Records(数据类,预览)。Switch表达式(表达式式switch,预览)。Pattern Matching for instanceof(预览)。文本块(多行字符串,"""语法)。14. Java 15(2020年9月15日)核心功能:增强预览特性。关键特性:Records正式版。文本块正式版。隐藏类(jdk.internal.vm.hidden)。密封类(sealed,预览)。15. Java 16(2021年3月16日)核心功能:新特性正式化。关键特性:密封类正式版。弃用remove()方法(集合的remove()改为removeIf())。Vector API(Incubator)(SIMD指令支持)。模式匹配增强(instanceof与类型匹配结合)。16. Java 17(2021年9月14日,LTS)核心功能:LTS版本,企业长期支持。关键特性:Sealed Classes正式版。Switch表达式正式版。移除Java EE模块(如java.xml.bind)。强封装JDK内部API(--add-opens)。Pattern Matching for instanceof正式版。17. Java 18(2022年3月15日)核心功能:改进性能和可维护性。关键特性:虚拟线程(Virtual Threads)(预览,轻量级线程)。结构化并发框架(StructuredConcurrent API)。模式匹配switch(switch支持类型匹配)。18. Java 19(2022年9月19日)核心功能:语言和API优化。关键特性:虚拟线程(Virtual Threads)第二版(改进调度)。结构化并发增强(StructuredConcurrent改进)。记录模式(Record Patterns)(解构record数据)。19. Java 20(2023年3月21日)核心功能:性能和工具改进。关键特性:虚拟线程(Virtual Threads)正式版。强封装JDK内部API增强(--illegal-access)。模式匹配switch正式版。Vector API第二版(SIMD优化)。表格总结:Java版本关键特性对比版本    发布时间    LTS    核心特性    关键功能Java 1.0    1996年1月    否    首个版本    跨平台、基础类库、Applet支持Java 1.1    1997年2月    否    内部类、反射、RMI    增强GUI和网络功能Java 1.2    1998年12月    否    Java 2命名、集合框架、Swing    GUI现代化、模块化结构Java 5    2004年9月    否    泛型、注解、枚举、增强for循环    语法革新,奠定现代Java基础Java 8    2014年3月    否    Lambda、Stream API、新日期API    函数式编程支持,生态转折点Java 9    2017年9月    否    模块化系统(JPMS)、JShell    模块化JDK,开发工具增强Java 11    2018年9月    是    HTTP客户端正式版、Epsilon GC    长期支持,企业级首选Java 14    2020年3月    否    Records、文本块、Switch表达式预览    预览特性快速迭代Java 17    2021年9月    是    Sealed Classes、Switch表达式正式版、移除Java EE模块    企业级LTS,语法和API现代化Java 20    2023年3月    否    虚拟线程正式版、模式匹配switch正式版    并发性能提升,SIMD优化关键总结LTS版本:Java 8、11、17 是企业长期支持版本。核心演进:语法革新:从Java 5的泛型到Java 8的Lambda,再到Java 14的Records。并发优化:Java 8的CompletableFuture到Java 17的虚拟线程。模块化:Java 9的JPMS彻底改变JDK结构。性能提升:G1 GC、ZGC、Vector API等持续优化。未来方向:虚拟线程(轻量级并发)、模式匹配、结构化并发框架的进一步发展。————————————————原文链接:https://blog.csdn.net/zp357252539/article/details/147495182
  • [技术干货] Java动态创建JSON不再难:GeoJSON完整实现指南
    前言        在当今数字化时代,数据的存储、传输与处理愈发依赖于灵活且高效的格式,JSON(JavaScript Object Notation)以其简洁、易读易写的特性脱颖而出,成为跨平台数据交换的首选格式之一。而在地理信息系统(GIS)领域,GeoJSON作为一种基于JSON的地理空间数据格式,为地理信息的表达与共享提供了强大支持。它能够以一种标准化的方式描述地理空间数据,包括点、线、面等几何对象以及与之相关的属性信息,广泛应用于地图绘制、空间分析、地理数据可视化等诸多场景。         Java作为一种功能强大、应用广泛的编程语言,在企业级应用开发、大数据处理、云计算等诸多领域占据着重要地位。随着地理空间数据应用的不断拓展,越来越多的Java开发者需要在项目中处理GeoJSON数据,例如从数据库动态生成GeoJSON数据以供前端地图应用展示,或者根据用户输入动态构建GeoJSON对象进行空间查询等。然而,对于许多Java开发者而言,动态创建JSON,尤其是结构相对复杂的GeoJSON,往往存在诸多困惑与挑战。如何在Java中高效、灵活地生成符合GeoJSON规范的数据,成为开发者亟待解决的问题。         本文将深入浅出地为读者呈现一份Java动态创建GeoJSON的完整实现指南。无论你是初涉GeoJSON的Java新手,还是希望在项目中优化GeoJSON处理流程的资深开发者,本文都将为你提供实用的思路与方法。我们将从Java处理JSON的基础讲起,介绍常用的JSON处理库,如Jackson、Gson等,并详细阐述它们在GeoJSON创建中的适用场景与优势。接着,深入剖析GeoJSON的结构组成,包括几何对象(点、线、多边形等)和属性部分,通过具体代码示例,逐步展示如何在Java中动态构建这些元素,实现从简单到复杂的GeoJSON对象生成。同时,结合实际应用场景,如地理数据的动态查询与转换为GeoJSON,探讨如何优化代码以提高性能和可维护性。 一、动态属性应用场景        本节将重点介绍动态属性的应用场景,以及需要考虑的一些问题。 1、场景介绍在面向GIS的业务场景中,我们通常可以将业务表中的列属性直接包装成Properties,然后通过后台返回给前端时,可以直接对这些数据进行展示。大家可以思考以下问题:假如一些属性信息在进行表连接查询时,并没有相关的业务表查询,而是要通过计算后才能给到前端的。这种情况下,我们还能只依靠纯SQL来解决这些问题吗?答案肯定是不行的,比如我们有一个场景,使用SQL的动态属性生成时,已经包含以下属性: String originalJson = "{\"type\" : \"Feature\", \"geometry\" : {\"type\":\"Point\",\"coordinates\":[113.902426,22.729881]}, \"properties\" : {\"id\" : 1369981, \"location\" : \"光明区玉塘街道文明路13号\", \"durationHours\" : 2}}";一键获取完整项目代码bash        然后我们需要在这个字符串中添加新的属性,这就是我们的使用场景。 2、需要考虑的问题        在实现这个需求的时候,需要考虑以下的问题,比如最简单的是如何实现简单的key-value的键值新增,更复杂一点的是如何实现嵌套对象的新增,还有更复杂的是如何实现嵌入的对象的新增。以上这些问题,都是需要我们考虑的,因此在本文后续的内容中我们都会进行实现和说明。 二、Java动态属性实现        本节将以Java语言为例,将从设计原则,Java核心类、编辑器的设计和从设计模式支持这几个角度进行介绍。让大家对这个动态属性生成实现有一个基本的认识。 1、设计原则这里我们使用面向对象的设计方法,因此设计的原则也是基本的OOP思想,即: /** * JSON属性操作工具类的面向对象设计 * 主要设计思想: * 1. 单一职责原则:每个类专注于一个特定功能 * 2. 开闭原则:扩展开放,修改关闭 * 3. 依赖倒置原则:依赖于抽象,而非具体实现 * 4. 组合优于继承:使用组合构建复杂功能 */一键获取完整项目代码bash2、核心类解析2.1主核心类 JsonPropertyManager/** * JsonPropertyManager - 外观模式(Facade Pattern) * 提供统一的静态接口,隐藏内部复杂性 * 设计原则:简化客户端调用,统一入口 */public class JsonPropertyManager {    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();        // 私有构造器:防止实例化,确保工具类的正确使用方式    private JsonPropertyManager() {        throw new IllegalStateException("工具类,无需实例化");    }        /**     * 静态工厂方法:创建JsonEditor实例     * 设计模式:工厂方法模式     * 好处:封装对象创建逻辑,便于后续扩展     */    public static JsonEditor createEditor(String jsonStr) throws JsonProcessingException {        return new JsonEditor(jsonStr);    }}一键获取完整项目代码java 2.2编辑器类:JsonEditor - 核心业务对象/** * JsonEditor - 建造者模式(Builder Pattern)+ 状态模式(State Pattern) *  * 职责: * 1. 封装JSON文档的编辑状态 * 2. 提供链式调用的API * 3. 管理当前操作的目标节点 *  * 面向对象特性: * - 封装:将JSON节点状态和操作封装在一起 * - 多态:支持多种数据类型操作 * - 聚合:组合了ArrayEditor等子组件 */public static class JsonEditor {    // 状态变量:封装对象状态    private final ObjectNode rootNode;      // 根节点 - 不变状态    private ObjectNode currentTargetNode;   // 当前目标节点 - 可变状态        /**     * 构造函数:初始化状态     * 面向对象原则:确保对象创建时处于有效状态     */    public JsonEditor(String jsonStr) throws JsonProcessingException {        this.rootNode = (ObjectNode) OBJECT_MAPPER.readTree(jsonStr);        this.currentTargetNode = rootNode; // 默认操作根节点    }        /**     * 目标节点设置方法 - 状态模式实现     * 允许动态切换操作上下文     */    public JsonEditor target(String nodePath) {        // 实现路径解析和节点定位逻辑        return this; // 返回this支持链式调用 - 流畅接口模式    }}一键获取完整项目代码java 2.3数组编辑器类:ArrayEditor - 组合模式应用/** * ArrayEditor - 组合模式(Composite Pattern) *  * 职责: * 1. 专门处理JSON数组操作 * 2. 提供类型安全的数组构建方法 * 3. 支持递归构建嵌套结构 *  * 设计理念:将数组操作从JsonEditor中分离,实现单一职责 */public static class ArrayEditor {    private final ArrayNode arrayNode; // 封装ArrayNode,提供更友好的API        /**     * 添加元素方法 - 支持多种数据类型,展示多态性     */    public ArrayEditor add(Object value) {        // 运行时类型检查和处理 - 运行时多态        if (value instanceof String) {            arrayNode.add((String) value);        } else if (value instanceof Map) {            // 处理Map类型 - 递归处理            arrayNode.add(OBJECT_MAPPER.valueToTree(value));        }        return this; // 链式调用支持    }        /**     * 添加对象到数组 - 命令模式(Command Pattern)元素     * 通过Consumer回调,实现灵活的配置     */    public ArrayEditor addObject(Consumer<JsonEditor> consumer) {        // 创建新对象节点        ObjectNode objectNode = OBJECT_MAPPER.createObjectNode();                // 使用临时JsonEditor配置对象        JsonEditor editor = new JsonEditor("{}") {            @Override            public ObjectNode getRootNode() {                return objectNode;            }        };                // 应用配置        consumer.accept(editor);        arrayNode.add(objectNode);                return this;    }}一键获取完整项目代码java 3、设计模式支持        这里将简单介绍在Json动态属性管理器设计中使用的一些设计模型。设计模式是个好方法,通过设计模式可以让代码设计更合理,扩展更方便。这里涉及的设计模式包含以下: 3.1建造者模式(Builder Pattern)/** * 建造者模式在工具类中的应用: *  * 特点: * 1. 分离复杂对象的构建和表示 * 2. 允许逐步构建复杂对象 * 3. 提供流畅的API接口 *  * 在JsonEditor中的体现: */public class JsonEditor {    // 链式调用示例    public JsonEditor add(String key, String value) {        currentTargetNode.put(key, value);        return this; // 返回this实现链式调用    }        public JsonEditor addMap(String key, Map<String, ?> map) {        currentTargetNode.set(key, OBJECT_MAPPER.valueToTree(map));        return this;    }        // 使用示例:流畅的API调用    JsonEditor editor = JsonPropertyManager.createEditor(jsonStr)        .target("properties")        .add("status", "处理中")        .addMap("contact", contactMap)        .addNestedObject("analysis", this::configureAnalysis);}一键获取完整项目代码java 3.2策略模式(Strategy Pattern)/** * 策略模式:通过函数式接口实现不同的数据处理策略 */public class JsonEditor {        /**     * 接受Consumer策略,对属性值执行自定义操作     */    public JsonEditor with(String key, Consumer<JsonNode> action) {        JsonNode node = currentTargetNode.get(key);        if (node != null) {            action.accept(node); // 执行策略        }        return this;    }        /**     * 接受Function策略,转换属性值     */    public JsonEditor transform(String key, Function<JsonNode, JsonNode> transformer) {        JsonNode node = currentTargetNode.get(key);        if (node != null) {            JsonNode transformed = transformer.apply(node); // 应用转换策略            currentTargetNode.set(key, transformed);        }        return this;    }        // 使用示例:应用不同的策略    editor.with("data", node -> {        // 自定义处理逻辑        System.out.println("Processing node: " + node);    });        editor.transform("array", node -> {        // 自定义转换逻辑        return node.isArray() ? node : OBJECT_MAPPER.createArrayNode();    });}一键获取完整项目代码java 3.3模板方法模式(Template Method Pattern)/** * 模板方法模式:定义算法骨架,具体步骤由子类或回调实现 *  * 在addNestedObject方法中的体现: */public class JsonEditor {        /**     * 模板方法:定义创建和配置嵌套对象的步骤     * 1. 创建嵌套对象节点     * 2. 保存当前状态     * 3. 应用配置(由consumer实现)     * 4. 恢复状态     * 5. 添加嵌套对象     */    public JsonEditor addNestedObject(String key, Consumer<JsonEditor> consumer) {        // 步骤1:创建嵌套对象        ObjectNode nestedNode = OBJECT_MAPPER.createObjectNode();        ObjectNode originalTarget = currentTargetNode; // 步骤2:保存状态                // 步骤3:应用配置(具体实现由consumer提供)        currentTargetNode = nestedNode;        consumer.accept(this);                // 步骤4:恢复状态        currentTargetNode = originalTarget;                // 步骤5:添加嵌套对象        currentTargetNode.set(key, nestedNode);                return this;    }}一键获取完整项目代码java         通过这些设计模式的使用,可以有效的提升我们的应用程序的实现。在需要扩展时非常方便。 三、调用实践        本节将基于动态属性管理独享来实现简单属性、嵌套属性、负责类型嵌入这几个方面来进行实例调用实践,为大家提供调用演示。 1、添加简单属性        首先来介绍如何添加简单属性,这是最简单的属性添加,可以理解成主要就是进行key_value的值映射。调用代码如下: // 原始JSON字符串String originalJson = "{\"type\" : \"Feature\", \"geometry\" : {\"type\":\"Point\",\"coordinates\":[113.902426,22.729881]}, \"properties\" : {\"id\" : 1369981, \"location\" : \"光明区玉塘街道文明路13号\", \"reason\" : \"故障\", \"startTime\" : \"10:13\", \"estimatedRestore\" : \"10:15\", \"durationHours\" : 2}}";      System.out.println("=== 原始JSON ===");System.out.println(originalJson);       System.out.println("\n=== 示例1: 添加List<Map<?>> - 不限定key ===");JsonEditor editor1 = JsonPropertyManager.createEditor(originalJson);   // 创建不同类型的List<Map>List<Map<String, Object>> poiList = new ArrayList<>();  // 第一个POI - 简单类型Map<String, Object> poi1 = new HashMap<>();poi1.put("name", "南山外国语学校");poi1.put("type", "学校");poi1.put("distance", 500);poi1.put("isPublic", true);poiList.add(poi1);       HashMap<String,Object> cotactMap = new HashMap<String, Object>();// 第二个POI - 包含嵌套对象Map<String, Object> poi2 = new HashMap<>();poi2.put("name", "某大型数据中心");poi2.put("type", "商业");poi2.put("capacity", "1000台服务器");cotactMap.put("person", "李主任");cotactMap.put("phone","13800138001");poi2.put("contact", cotactMap);poiList.add(poi2);// 添加POI列表到propertieseditor1.target("properties").addListMap("majorPOIs", poiList);一键获取完整项目代码java 2、添加嵌套类型        如果有嵌套类型,属性添加进来则会有一些问题。可以使用以下方法来进行动态添加,代码如下: JsonEditor editor2 = JsonPropertyManager.createEditor(originalJson);editor2.target("properties")    .addNestedObject("analysis", nested -> {        nested.add("riskLevel", "中")              .add("impactRadius", 1000)              .addNestedArray("affectedServices", services -> {                   services.add("电力供应")                         .add("网络通信")                         .add("安防系统");                            })                            .addNestedObject("timeline", timeline -> {                                timeline.add("detectionTime", "10:10")                                       .add("dispatchTime", "10:20")                                       .add("estimatedCompletion", "12:00");                            });                  })                  .addNestedArray("repairTeams", teams -> {                      try {teams.addObject(team -> {      team.add("name", "光明供电局抢修一队")          .add("members", 5)          .add("equipment", Arrays.asList("绝缘杆", "万用表", "工具箱"));  })  .addObject(team -> {      team.add("name", "技术支持小组")          .add("members", 3)          .add("specialties", Arrays.asList("变压器维修", "线路检测"));  });} catch (JsonProcessingException e) {// TODO Auto-generated catch blocke.printStackTrace();}                  });                        System.out.println(editor2.toPrettyJson());一键获取完整项目代码java         通过以上方法基本就可以实现Json的动态属性管理,如果需要更复杂的属性添加可以根据方法来进行添加。篇幅有限,这里不进行赘述。大家如果对如何进行Json的动态属性扩展感兴趣,在自己的项目中可能会遇到这类问题,可以下载源码:Java实现JSON的动态属性添加源码。里面代码大家可以自行进行优化。         程序调用完成后,可以在控制台看到以下输出:   四、总结        以上就是本文的主要内容,本文将深入浅出地为读者呈现一份Java动态创建GeoJSON的完整实现指南。无论你是初涉GeoJSON的Java新手,还是希望在项目中优化GeoJSON处理流程的资深开发者,本文都将为你提供实用的思路与方法。通过阅读本文,你将不仅掌握Java动态创建GeoJSON的技术细节,更将理解其背后的原理与最佳实践,从而在实际项目中能够灵活运用,轻松应对各种与GeoJSON相关的开发任务,让Java动态创建GeoJSON变得不再困难。行文仓促,定有不足之处,欢迎各位朋友在评论区批评指正,不胜感激。————————————————原文链接:https://blog.csdn.net/yelangkingwuzuhu/article/details/156097415
  • [技术干货] 别再只知道 UUID 了!分布式 ID 生成方案大盘点与 Java 实现
    最近在深入学习 Java 后端和 Redis 中间件时,遇到了一个非常经典且重要的问题:在分布式场景下,如何生成一个全局唯一的 ID?在单体架构时代,我们习惯使用数据库的自增 ID(Auto Increment),但在分库分表、微服务的高并发场景下,这种方式由于性能瓶颈和单点问题,显然已经力不从心。今天这篇博客就来总结一下目前业界最主流的 4 种全局唯一 ID 生成策略,分析它们的原理、优缺点以及适用场景。什么样的 ID 才是好 ID?在设计 ID 生成器之前,我们需要明确“好 ID”的标准。通常有以下几个核心要求:全局唯一性:这是最基本的要求,不能出现重复。高可用 & 高性能:生成 ID 的动作非常频繁,不能成为系统的瓶颈,且服务要足够稳定。递增性(趋势有序):这一点常被忽略。对于使用 MySQL(InnoDB 引擎)的系统,主键建议保持递增,因为 InnoDB 使用 B+ 树索引,有序的主键写入能避免频繁的“页分裂”,极大提升写入性能。安全性:某些业务场景下(如订单号),ID 不应过于明显地暴露业务量(比如不能让人轻易猜出你一天有多少单)。方案一:UUID (Universally Unique Identifier)UUID 是最简单、最暴力的方案。JDK 原生支持,一行代码搞定。代码实现public static void main(String[] args) {    // 生成一个 UUID,并去掉中间的横线    String id = UUID.randomUUID().toString().replace("-", "");    System.out.println("UUID: " + id);}一键获取完整项目代码java优缺点分析优点:性能极高:完全在本地生成,没有网络消耗。使用简单:不依赖任何外部组件(DB、Redis 等)。缺点:无序性(致命伤):UUID 是无序的字符串。如果作为 MySQL 主键,会导致大量的数据页分裂和移动,严重拖慢插入速度。存储成本高:32 个字符(或 16 字节),相比 Long 类型特别占空间,也会导致索引变大。信息不安全:完全随机,无法携带时间或业务含义。结论:适合生成 Token、Session ID 或非数据库主键的场景。坚决不建议用作 MySQL 的主键。方案二:数据库自增 (Database Auto-Increment)利用 MySQL 的 auto_increment 特性,或者 Oracle 的 Sequence。原理应用服务向数据库插入数据,数据库自动累计 ID。优缺点分析优点:简单:利用现有数据库功能,成本低。单调递增:对索引非常友好,查询效率高。缺点:并发瓶颈:在高并发下,数据库往往是最大的瓶颈。分库分表麻烦:如果未来需要分库,不同库的自增 ID 会重复。虽然可以通过设置不同的“步长”(Step)来解决(如 DB1 生成 1,3,5... DB2 生成 2,4,6...),但这增加了扩容和维护的难度。单点故障:数据库挂了,整个 ID 生成服务就不可用了。结论:适合并发量不高的中小项目,或者不需要分库分表的数据表。方案三:Redis 自增策略Redis 是单线程处理命令的,其 INCR 命令是原子的,天生适合做计数器。这是我最近在学 Redis 时觉得非常有意思的一个应用点。代码思路 (Java + RedisTemplate)为了避免 ID 被推测出业务量,通常会结合“时间戳”使用。格式示例:yyyyMMdd + Redis自增值。// 伪代码示例public long generateId(String keyPrefix) {    // 1. 生成时间戳部分    String dateStr = DateTimeFormatter.ofPattern("yyyyMMdd").format(LocalDate.now());        // 2. 利用 Redis 原子递增    // key 举例: icr:order:20251216    Long increment = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + dateStr);        // 3. 拼接 ID (实际生产中通常需要通过位运算或字符串填充补齐位数)    return Long.parseLong(dateStr + String.format("%06d", increment));}一键获取完整项目代码java优缺点分析优点:高性能:基于内存操作,吞吐量远高于数据库。有序递增:对数据库索引友好。灵活:可以方便地把日期、业务类型编排进 ID 中。缺点:强依赖组件:如果 Redis 挂了,ID 生成服务就断了(需要配置 Sentinel 或 Cluster 高可用)。运维成本:引入了额外的中间件维护成本。 结论:非常适合高并发的业务场景(如秒杀、订单生成),且生成的 ID 具有业务含义。方案四:雪花算法 (Snowflake)这是目前分布式系统中最流行、最成熟的方案,由 Twitter 开源。它的核心思想是将一个 64 位的 long 型数字切割成不同的部分。结构图解 (64 bit)1 bit:符号位(固定为0)。41 bits:时间戳(毫秒级,可以使用 69 年)。10 bits:机器 ID(支持 1024 个节点)。12 bits:序列号(同一毫秒内支持生成 4096 个 ID)。代码实现通常不需要自己手写位运算,推荐使用成熟的工具包,例如 Hutool。// 引入 Hutool 依赖后public class IdTest {    public static void main(String[] args) {        // 参数1: 终端ID, 参数2: 数据中心ID        Snowflake snowflake = IdUtil.getSnowflake(1, 1);        long id = snowflake.nextId();        System.out.println("Snowflake ID: " + id);    }}一键获取完整项目代码java优缺点分析优点:极高并发:每秒可生成几百万个 ID。不依赖网络:本地生成(除了启动时校验机器 ID),无单点故障。趋势递增:整体按时间递增,索引性能好。缺点:时钟回拨问题:严重依赖服务器时间。如果服务器时间被回调(比如校准时间),算法可能会生成重复 ID。 结论:几乎所有互联网大厂的主流选择,适合超大规模的分布式系统。总结对比最后,用一张表来总结这几种策略:策略    唯一性    有序性    性能    依赖组件    核心痛点UUID    高    无    极高    无    索引性能差,ID太长DB自增    高    严格有序    低    数据库    并发瓶颈,扩展麻烦Redis    高    严格有序    高    Redis    依赖 Redis 高可用Snowflake    高    趋势有序    极高    无    时钟回拨问题个人建议:如果你是初学者或者项目规模较小,Redis 自增是一个非常好的练手方案,既能满足性能要求,又能加深对 Redis 的理解。而如果是企业级的大型项目,Snowflake(配合 Hutool 等工具库)则是目前的最优解。希望这篇总结对大家有所帮助!如果你有更好的方案,欢迎在评论区交流。————————————————原文链接:https://blog.csdn.net/m0_58782205/article/details/155989061
  • [技术干货] 【入门篇】一键搞定 Java 环境配置,从 0 跑出你的第一个程序
    1. Java概述Java 是一种跨平台、面向对象的高级编程语言,广泛应用于企业级开发、大数据、移动端等领域。1.1 什么是 Java语言:人和人交流用的工具,比如中文、英文。计算机语言:人与计算机之间信息交流沟通的一种特殊语言,用来告诉计算机应该做什么。Java:一种非常流行的计算机语言,我们可以通过 Java 代码一步步指挥计算机完成各种任务。2. 环境准备这里需要配置 JDK。2.1 JDK的配置2.1.1 JDK概述JDK(Java Development Kit)称为 Java 开发工具,包含了 JRE 和开发工具。在这里,我将带大家快速配置 JDK。注意:针对不同的操作系统,需要下载对应版本的 JDK。如果电脑是 Windows 32 位的,建议重装成 64 位操作系统。因为 Java 从 9 版本开始,不再提供 32 位的安装包。2.1.2 快速下载打开 Oracle 官网下载地址:Oracle快速传送门选择 JDK21 版本,这里有 Linux、macOS、Windows 三种操作系统,根据电脑版本选择下载。一般只要是 LTS 版本(如 17 或 21) 都可以正常学习使用。Windows 其他版本(JDK8、17、21)下载:百度网盘快速传送门这里以 Windows 为例,点击 Installer,下载即可。 傻瓜式安装,下一步即可。默认的安装路径是在 C:\Program Files 下。建议自己创建一个文件夹,将开发相关的东西都塞进去,便于管理与查找。该文件夹上的所有路径,不能有中文,不能有特殊符号,不能有空格。2.1.3 环境配置配置环境变量的作用:如果我想在电脑的任意目录下,都可以启动一个软件,那么就可以把这个软件的路径配置到环境变量中。在启动软件的时候,操作系统会先在当前路径下找,如果在当前路径没有找到,再到环境变量的路径去找。如果都找不到就提示无法启动。具体配置方法:快捷指令:打开 Win+R输入 sysdm.cpl 回车选择“高级”,再点击下面的“环境变量”手动查找:右键“此电脑”,选择“属性”点击左侧的“高级系统设置”选择“高级”,再点击下面的“环境变量”点击“新建”,将 JDK 安装目录写进去,变量名起 JAVA_HOME点击 Path,点击“编辑”点击“新建”,填写 %JAVA_HOME%\bin将 %JAVA_HOME%\bin 移到最上面移动的好处:在 CMD 中打开软件时,会先找当前路径,再找环境变量,在环境变量中从上往下依次查找,如果路径放在最上面,查找最快。2.1.4 检查是否安装成功打开 Win+R输入 cmd输入 java -version注意:java -version 中间有一个空格如果有以下效果,则成功,21.0.6 是我的 JDK 版本。java version "21.0.6" 2025-01-21 LTSJava(TM) SE Runtime Environment (build 21.0.6+8-LTS-188)Java HotSpot(TM) 64-Bit Server VM (build 21.0.6+8-LTS-188, mixed mode, sharing)一键获取完整项目代码shell拓展:Win10 可能会有个 BUG,当电脑重启后,环境变量可能会失效。如果你遇到这种情况,可以在 PATH 中直接添加完整的 JDK bin 目录来解决。2.2 IDEA的配置如果你是在学校,可能会让你使用 Eclipse,但我强烈建议你使用 IDEA。2.2.1 IDEA 概述IDEA 全称 IntelliJ IDEA,是用于 Java 语言开发的集成环境,它是业界公认的目前用于 Java 程序开发最好的工具。集成环境:把代码编写、编译、执行、调试等多种功能综合到一起的开发工具。2.2.2 快速下载和配置IDEA 下载链接:IDEA官网下载快速传送门下载完,进行安装,将目录改为自己的文件夹。其余只需要勾选创建快捷方式,其他的一直 next 即可。 注:IDEA 是需要付费的,个人使用每年约 1400 元,支持正版人人有责。因此,请大家务必不要刻意到网上寻找对应版本 IDEA 激活码进行激活。3. IDEA 介绍3.1 IDEA 概述IntelliJ IDEA 是一款强大的 Java 集成开发环境(IDE),被广泛认为是开发 Java 程序的最佳工具之一。在开始本文档之前,请确保你已正确安装 JDK 和 IDEA。3.2 项目层级结构在 IDEA 中,Java 项目按以下层级进行组织:project(项目、工程):比如 QQ、微信等应用。module(模块):项目的功能子模块,如聊天、通讯录。package(包):模块内的业务分类,如聊天模块中的朋友圈、视频号等。class(类):具体的代码单元,实际开发主要在类中完成。3.3 快速入门创建一个新的项目需要遵循以下层级:创建项目(Project)新建包(Package)新建类(Class)创建一个新的项目在弹出的窗口中:选择项目类型:Java填写项目名称和路径Name:项目名称Location:项目存储路径add sample code:取消添加简单代码选项右键 src 文件夹选择 New → Java Class输入类名,如 HelloWorld回车确认输入以下代码:public class HelloWorld {    public static void main(String[] args) {        System.out.println("Hello,World");    }}一键获取完整项目代码java右键类文件,点击“运行”即可。 这就完成了第一个程序的运行。代码解析:// class:定义一个类,后面跟随类名// HelloWorld:类的名字,通常与文件名一致// {}:类的范围,代码需要在范围中书写public class HelloWorld {    // 主程序入口(main 方法)    // 程序运行会从这里开始,从上往下依次执行    // 在 IDEA 中,可以通过输入 psvm 快速生成    public static void main(String[] args) {        // 输入语句        // 作用:将括号内的内容打印到控制台        // 快速生成:sout        System.out.println("Hello,World");    }}一键获取完整项目代码java结语在本文中,我们掌握了以下知识点:安装了 Java 开发环境:JDK 和 IDEA。编写并运行了第一个 Java 程序:HelloWorld。初步认识了 class、main 等基础关键字的作用。如果本文对你有帮助:欢迎点赞、收藏,让更多正在学 Java 的同学看到。遇到问题或有不同理解:可以在评论区留言,一起讨论、互相学习。想系统看更多内容:可以关注专栏《Java成长录》,一起把基础打牢。————————————————原文链接:https://blog.csdn.net/Chase_______/article/details/155501655
  • [技术干货] 【Java 开发日记】阻塞队列有哪些?拒绝策略有哪些?
    阻塞队列有哪些?在Java的java.util.concurrent包里面,阻塞队列的实现挺多的,我们可以根据它的功能和结构来记,主要分这么几类:1. 按容量划分:有界队列: 就是队列有固定的容量。ArrayBlockingQueue: 最经典的一个,底层是数组,创建时必须指定大小。它的生产和消费用同一把锁,性能相对稳定。LinkedBlockingQueue: 底层是链表,它既可以是有界的(构造时指定容量),也可以默认是无界的(默认是Integer.MAX_VALUE,几乎相当于无界)。它的生产和消费用了两把锁,在高并发场景下吞吐量通常比ArrayBlockingQueue更高。无界队列: 理论上是无限的,只要内存够就能一直放。PriorityBlockingQueue: 一个支持优先级排序的无界队列。元素必须实现Comparable接口,或者构造时传入Comparator。它出队的顺序是按优先级来的,不是先进先出DelayQueue: 一个很特殊的队列,里面放的是实现了Delayed接口的元素。每个元素都有个到期时间,只有到期了的元素才能被取出来。典型应用就是做缓存过期、定时任务调度。2. 特殊功能的队列:SynchronousQueue: 这个队列非常特别,它不存储元素。每一个put操作必须等待一个take操作,就像“手递手”交接一样。它直接传递任务,效率很高,常用于线程池之间直接传递工作。CachedThreadPool用的就是它。LinkedTransferQueue: 是LinkedBlockingQueue和SynchronousQueue的结合体。它多了个transfer方法,如果当前有消费者在等待,就直接把元素给消费者;如果没有,就入队,并且会阻塞直到该元素被消费掉。性能很好。所以,我比较常记的是ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、DelayQueue和SynchronousQueue这几个,它们各有各的应用场景。拒绝策略有哪些?拒绝策略是线程池的一个组成部分。当线程池的工作队列满了,并且所有线程也都达到最大线程数了,这时候再来新任务,就会触发拒绝策略。Java在ThreadPoolExecutor类里提供了4种内置的策略,都实现了RejectedExecutionHandler接口:1. AbortPolicy(中止策略 - 默认策略):做法: 直接抛出一个RejectedExecutionException异常。感受: “比较粗暴,但好处是能让我们及时感知到系统出了饱和问题。”2. CallerRunsPolicy(调用者运行策略):做法: 不抛弃任务,也不抛异常,而是将任务回退给调用者(提交任务的线程),让调用者自己去执行这个任务。感受: “这是一种‘负反馈’机制。提交任务的线程突然要自己去干活,它就忙起来了,自然就慢下来提交新任务了,给了线程池一个喘息的机会。这个策略在生产环境挺实用的,能平滑地降低流量。”3. DiscardPolicy(丢弃策略):做法: 默默地把新提交的任务丢弃掉,不执行,也不给任何通知。感受: “风险比较大,因为任务丢了我们都不知道。除非是一些无关紧要的场景,否则一般不推荐。”4. DiscardOldestPolicy(丢弃最老策略):做法: 把工作队列里排队时间最长的那个任务(队头的任务)丢掉,然后尝试把新任务再放进队列。感受: 这个策略有点‘喜新厌旧’。它可能丢弃掉一个非常重要的老任务,风险也挺高的,用的时候得想清楚业务上能不能接受。当然,如果这四种都不满足需求,我们还可以自己实现RejectedExecutionHandler接口,来自定义拒绝策略,比如把拒绝的任务持久化到磁盘,或者记录日志后发个告警等等。面试回答Java中的阻塞队列主要有像ArrayBlockingQueue这种基于数组的有界队列,也有LinkedBlockingQueue这种基于链表可以无界的队列。还有一些特殊用途的,比如按优先级出队的PriorityBlockingQueue,做延时任务的DelayQueue,以及不存元素专门做传递的SynchronousQueue。 关于拒绝策略,是线程池满载后的处理方式。默认的AbortPolicy会直接抛异常;CallerRunsPolicy会让提交任务的线程自己去执行,这个我们项目在用,能平滑流量;还有两种是直接丢弃任务或者丢弃队列中最老的任务。如果都不行,我们也可以根据业务自己实现一个。————————————————原文链接:https://blog.csdn.net/2402_87298751/article/details/155534236
  • [技术干货] 生命之树(Java)
    前言:这是一个一个经典的树形动态规划(Tree DP)问题,通常被称为 “最大子树和” 或 “带权树的最大连通子图和”采用动态规划和贪心算法!题目:在 X 森林里,上帝创建了生命之树。他给每棵树的每个节点(叶子也称为一个节点)上,都标了一个整数,代表这个点的和谐值。上帝要在这棵树内选出一个节点集 S,使得对于 SS 中的任意两个点 a,b,都存在一个点列 a,v1,v2,⋯,vk,b 使得这个点列中的每个点都是 S 里面的元素,且序列中相邻两个点间有一条边相连。在这个前提下,上帝要使得 S 中的点所对应的整数的和尽量大。这个最大的和就是上帝给生命之树的评分。经过 atm 的努力,他已经知道了上帝给每棵树上每个节点上的整数。但是由于 atm 不擅长计算,他不知道怎样有效的求评分。他需要你为他写一个程序来计算一棵树的分数。集合 S 可以为空。输入描述第一行一个整数 n 表示这棵树有 n 个节点。第二行 n 个整数,依次表示每个节点的评分。接下来 n−1 行,每行 2 个整数 u,v,表示存在一条 u 到 v 的边。由于这是一棵树,所以是不存在环的。其中,0<n≤105, 每个节点的评分的绝对值不超过 106。输出描述输出一行一个数,表示上帝给这棵树的分数。输入输出样例示例输入51 -2 -3 4 54 23 11 22 5一键获取完整项目代码输出8一键获取完整项目代码运行限制最大运行时间:3s最大运行内存: 256M题目分析:一、题目描述给定一棵包含 n 个节点的无向树,每个节点有一个整数值(可正可负)。要求选择一个连通的子图(即若干相连的节点),使得这些节点的权值之和最大。返回这个最大和。二、问题输入结构:一棵树(无环连通无向图),可用邻接表表示。目标:找一个连通的节点集合,使其权值和最大。关键约束:所选节点必须构成连通子图(不能跳着选)。注意:这不是“最大独立集”,也不是“最长路径”,而是带权最大连通子图。三、观察树的性质:任意两个节点间有唯一路径 → 任何连通子图也是一棵树。最优解必有“根”:最大连通子图一定存在一个“最高点”(在 DFS 遍历时最先被访问的节点)。局部决策影响全局:若某个子树的总贡献为负,则不应纳入当前解。四、算法步骤构建邻接表存储树。从任意节点(如 1)开始 DFS。对每个节点:初始化当前和为自身权值;递归处理子节点;仅累加正贡献的子树;更新全局最大值;返回当前和供父节点使用。输出 maxSum。代码:package com.itdonghuang.Test; import java.util.*; public class JavaTest1 {    static long maxsum = 0;     public static void main(String[] args) {        Scanner scan = new Scanner(System.in);                int n = scan.nextInt();                int[] val = new int[n + 1];        for (int i = 1; i <= n; i++) {            val[i] = scan.nextInt();        }         List<Integer>[] opp = new ArrayList[n + 1];        for (int i = 1; i <= n; i++) {            opp[i] = new ArrayList<>();        }        for (int i = 0; i < n - 1; i++) {            int u = scan.nextInt();            int v = scan.nextInt();            opp[u].add(v);            opp[v].add(u);        }         dfs(1, -1, opp, val);        System.out.println(maxsum);         scan.close();    }     public static long dfs(int u, int parent, List<Integer>[] opp, int[] val) {        long sum = val[u];         for (int v : opp[u]) {            if (v == parent) continue;                        long childSum = dfs(v, u, opp, val);            if (childSum > 0) {                sum += childSum;            }        }         if (sum > maxsum) {            maxsum = sum;        }        return sum;    }}一键获取完整项目代码java代码分析:一、初始化,赋值定义n、val、opp分别接受键盘输入的节点数、每个结点的评分、u<-->v的边opp是一个数组array,每个元素是List<Integer>,构成无向图Scanner scan = new Scanner(System.in);        int n = scan.nextInt();        int[] val = new int[n + 1];for (int i = 1; i <= n; i++) {    val[i] = scan.nextInt();} List<Integer>[] opp = new ArrayList[n + 1];for (int i = 1; i <= n; i++) {    opp[i] = new ArrayList<>();}for (int i = 0; i < n - 1; i++) {    int u = scan.nextInt();    int v = scan.nextInt();    opp[u].add(v);    opp[v].add(u);}一键获取完整项目代码java二、dfs方法讲解 逐行详解第 1 行:long sum = val[u];含义:当前子树至少包含节点 u 自己。用 long,防止整数溢出(权值可能很大)。关键思想:我们计算的是 “必须包含当前节点 u 的最大连通子图和”。注意:这个子图必须包含 u,但可以选择性地包含它的某些子树。第 2–3 行:遍历邻居 + 跳过父节点for (int v : opp[u]) {    if (v == parent) continue;一键获取完整项目代码javaopp[u] 是节点 u 的所有邻居(来自邻接表)。因为树是无向图,u 的邻居包括它的父节点和子节点。但我们是从根往下 DFS 的,所以要避免走回父节点,否则会无限递归或重复访问。举例:如果从 1 → 2 → 3,那么在 dfs(2, 1) 中,opp[2] 包含 1 和 3,必须跳过 1(因为它是父节点),只处理 3。第 4 行:long childSum = dfs(v, u, opp, val);递归调用:进入子节点 v,并告诉它:“你的父节点是 u”。返回值含义:childSum = 以 v 为根的子树中,包含 v 的最大连通子图的和。再次强调:这个值必须包含 v,但可能只包含 v 自己(如果子树都是负的)。第 5 行:if (childSum > 0) sum += childSum;这是整个算法最核心的贪心思想!为什么只加正数?如果某个子树的最大和是 负数(比如 -5),把它加到当前节点只会让总和变小。所以我们只“吸收”那些能带来正收益的子树。这相当于:不选那些负贡献的子树分支。类比数组版“最大子数组和”(Kadane 算法):如果前缀和 < 0,就丢掉,重新开始。这里同理:如果子树和 < 0,就“断开”,不选它。举个例子:val[u] = 3子树 A 贡献 +4 → 加上 → 总和变成 7子树 B 贡献 -2 → 不加 → 总和还是 7最终 sum = 3 + 4 = 7第 6 行:if (sum > maxsum) maxsum = sum;更新全局答案!sum 是“包含当前节点 u 的最大连通子图和”。而全局最优解一定是以某个节点为“最高点”的连通子图(因为树是连通无环的)。所以我们在每个节点都尝试一次,取最大值。这就是为什么不需要额外判断“路径是否跨子树”——因为任何连通子图都有一个“顶部节点”,我们会在那里计算它。第 7 行:return sum;返回值用途:告诉父节点,“如果你把我(以 u 为根的这部分)接上去,最多能给你增加 sum 的收益”。父节点会根据这个值决定是否“吸收”你。这是一个典型的 自底向上(bottom-up) 的信息传递过程。public static long dfs(int u, int parent, List<Integer>[] opp, int[] val) {    long sum = val[u]; // 当前子树至少包含自己     for (int v : opp[u]) {        if (v == parent) continue; // 避免回溯到父节点(防止死循环)         long childSum = dfs(v, u, opp, val); // 递归计算以 v 为根的子树的最大“贡献值”         if (childSum > 0) {            sum += childSum; // 只有当子树贡献为正时,才合并进来        }    }     if (sum > maxsum) {        maxsum = sum; // 更新全局最大子树和    }     return sum; // 返回以 u 为根的子树能向上提供的最大和(用于父节点决策)}一键获取完整项目代码java结语:今天是我刷算法的第N天,每天刷算法题,写写项目,补充知识点。完善知识体系,加油加油!希望可以帮助到你!————————————————原文链接:https://blog.csdn.net/speaking_me/article/details/156127697
总条数:692 到第
上滑加载中