3545 字
18 分钟
java学习笔记(6)

注解#

Java注解(Annotation)是一种用于提供元数据(metadata)的机制,它允许我们在代码中嵌入补充信息,这些信息可以在编译时、运行时或部署时被读取和使用。注解不会改变程序的运行逻辑,但可以改变程序的处理方式。

注解的基本概念#

  • 注解定义:注解是通过@interface关键字定义的。
  • 注解使用:注解可以用于类、方法、字段、参数、局部变量等位置。
  • 注解Retention:注解的保留策略,指定注解在什么时期有效(源码阶段、编译阶段、运行时)。
  • 注解Target:注解的目标,指定注解可以用于哪些元素。

内置注解#

Java提供了几种内置注解:

  • @Override:表示一个方法声明旨在重写超类中的另一个方法。
  • @Deprecated:表示不推荐使用该元素,通常是因为它很危险或存在更好的选择。
  • @SuppressWarnings:表示在编译期间忽略指定的警告。

自定义注解#

可以创建自定义注解来满足特定需求:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface MyAnnotation {
    String value() default "default";
    int count() default 0;
}

注解元素#

注解可以包含元素,类似于方法:

  • 元素类型:可以是基本类型、String、Class、枚举、注解或这些类型的数组。
  • 默认值:可以为元素提供默认值。

注解的使用#

@MyAnnotation(value = "example", count = 10)
public class MyClass {
    @MyAnnotation
    public void myMethod() {
    }
}

注解处理#

注解本身不包含行为,它们的行为是通过注解处理器来实现的。注解处理器可以在编译时或运行时读取注解并执行相应操作。

  • 编译时处理:使用注解处理器(Annotation Processor)在编译时读取注解并生成代码或执行检查。
  • 运行时处理:通过反射在运行时读取注解并执行操作。

示例:运行时注解处理#

public class AnnotationProcessor {
    public static void main(String[] args) throws NoSuchMethodException {
        MyClass obj = new MyClass();
        if (obj.getClass().isAnnotationPresent(MyAnnotation.class)) {
            MyAnnotation annotation = obj.getClass().getAnnotation(MyAnnotation.class);
            System.out.println("Value: " + annotation.value());
            System.out.println("Count: " + annotation.count());
        }
    }
}

注解的优点#

  • 提供元数据:为代码提供额外的信息,而不改变代码本身。
  • 简化代码:减少编写重复代码的需要。
  • 增强功能:通过注解可以实现一些特殊功能,如依赖注入、权限控制等。

注解的局限性#

  • 性能影响:运行时注解处理可能影响性能。
  • 复杂性:过度使用注解可能导致代码复杂度增加。 注解是Java中一个非常强大和灵活的特性,它可以用于各种场景,如框架开发、代码生成、验证等。正确使用注解可以大大提高开发效率和代码质量。

FunctionalInterface#

在Java中,FunctionalInterface是一个用于标注接口的注解,它表示该接口是一个函数式接口。函数式接口是Java 8引入的一个新概念,它是指只包含一个抽象方法的接口。这样的接口可以用来表示lambda表达式或者方法引用。

FunctionalInterface的特性#

  1. 单一抽象方法:函数式接口只能有一个抽象方法。但是它可以包含多个默认方法(default method)和静态方法(static method)。
  2. 允许继承方法:如果接口继承了另一个接口的抽象方法,那么这个方法不会计入抽象方法的总数内。
  3. Object类的方法:函数式接口可以包含Object类的方法,如toString()equals()等,这些方法不会计入抽象方法的总数内。

FunctionalInterface的使用#

使用@FunctionalInterface注解可以确保接口满足函数式接口的定义。如果接口中定义了超过一个抽象方法,编译器将会报错。

@FunctionalInterface
public interface SimpleFunction {
    // 单一抽象方法
    int apply(int value);
    // 默认方法,不会影响FunctionalInterface的单一抽象方法规则
    default void printValue(int value) {
        System.out.println(value);
    }
    // 静态方法,同样不会影响
    static void printMessage(String message) {
        System.out.println(message);
    }
    // Object类的方法,不会计入抽象方法总数
    @Override
    String toString();
}

FunctionalInterface的好处#

  1. 保证接口的函数式特性:通过编译器检查,确保接口只有一个抽象方法,适合用于lambda表达式。
  2. 提高代码可读性:使用@FunctionalInterface注解的接口,其目的和用法更加明确,提高了代码的可读性。
  3. 支持泛型:函数式接口可以定义泛型方法,增加了接口的灵活性和适用性。

常见的FunctionalInterface#

Java 8的java.util.function包中定义了许多常用的函数式接口,如:

  • Predicate<T>:用于表示一个参数的布尔表达式。
  • Consumer<T>:表示一个接受一个参数并且没有返回值的操作。
  • Function<T, R>:表示一个接受一个参数并且返回结果的操作。
  • Supplier<T>:表示一个没有参数但是返回结果的操作。
  • UnaryOperator<T>:表示一个接受一个参数并且返回相同类型结果的操作。
  • BinaryOperator<T>:表示一个接受两个相同类型的参数并且返回相同类型结果的操作。

示例#

import java.util.function.Function;
public class FunctionalInterfaceExample {
    public static void main(String[] args) {
        // 使用lambda表达式实现Function接口
        Function<Integer, Integer> square = x -> x * x;
        System.out.println(square.apply(5)); // 输出25
    }
}

在这个示例中,我们定义了一个Function<Integer, Integer>类型的变量square,它接受一个Integer类型的参数并返回一个Integer类型的结果。我们使用lambda表达式x -> x * x来表示这个函数,它将输入的数平方后返回。 通过@FunctionalInterface,Java提供了一种简洁、高效的方式来表示和操作函数式编程中的函数。这在Java 8及以后的版本中极大地增强了Java语言的表达能力和灵活性。

ArrayList初始化#

在Java中,ArrayList 是一个常用的动态数组实现,它允许你存储和操作对象。当你想要在定义时给 ArrayList 赋值,你可以使用多种方法,包括使用匿名内部类、使用双括号初始化(注意可能带来的潜在问题)或者使用Java 9及以上版本的集合工厂方法。 以下是一些示例:

1. 使用匿名内部类#

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Main {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>() {{
            add("Apple");
            add("Banana");
            add("Cherry");
        }};
        System.out.println(list);
    }
}

2. 使用双括号初始化(不推荐用于多线程环境)#

import java.util.ArrayList;
import java.util.List;
public class Main {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>() {{
            add("Apple");
            add("Banana");
            add("Cherry");
        }};
        System.out.println(list);
    }
}

注意:双括号初始化会创建一个匿名内部类,这可能会导致内存泄漏或者在序列化时出现问题。

3. 使用Java 9的集合工厂方法#

如果你使用的是Java 9或更高版本,你可以使用List.of()方法来创建一个不可变的列表:

import java.util.List;
public class Main {
    public static void main(String[] args) {
        List<String> list = List.of("Apple", "Banana", "Cherry");
        System.out.println(list);
    }
}

注意List.of()创建的列表是不可变的,即不能添加、删除或替换元素。

4. 使用Arrays.asList(注意返回的列表是固定大小的)#

import java.util.Arrays;
import java.util.List;
public class Main {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("Apple", "Banana", "Cherry");
        System.out.println(list);
    }
}

注意Arrays.asList()返回的列表是固定大小的,不支持添加或删除操作,但可以修改元素。

HashMap初始化#

是的,HashMap也有类似的写法。在Java中,HashMap是一个常用的键值对存储结构,你可以在定义时给它赋值。以下是一些示例:

1. 使用匿名内部类#

import java.util.HashMap;
import java.util.Map;
public class Main {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<String, String>() {{
            put("key1", "value1");
            put("key2", "value2");
            put("key3", "value3");
        }};
        System.out.println(map);
    }
}

2. 使用双括号初始化(不推荐用于多线程环境)#

import java.util.HashMap;
import java.util.Map;
public class Main {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<String, String>() {{
            put("key1", "value1");
            put("key2", "value2");
            put("key3", "value3");
        }};
        System.out.println(map);
    }
}

3. 使用Java 9及以上的集合工厂方法#

import java.util.Map;
public class Main {
    public static void main(String[] args) {
        Map<String, String> map = Map.of(
            "key1", "value1",
            "key2", "value2",
            "key3", "value3"
        );
        System.out.println(map);
    }
}

注意:Map.of()创建的映射是不可变的,即不能添加、删除或替换键值对。

4. 使用Stream API(Java 8及以上)#

import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Main {
    public static void main(String[] args) {
        Map<String, String> map = Stream.of(
            new AbstractMap.SimpleEntry<>("key1", "value1"),
            new AbstractMap.SimpleEntry<>("key2", "value2"),
            new AbstractMap.SimpleEntry<>("key3", "value3")
        ).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
        System.out.println(map);
    }
}

5. 使用Collections.unmodifiableMap(创建不可变映射)#

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class Main {
    public static void main(String[] args) {
        Map<String, String>modifiableMap = new HashMap<>();
        modifiableMap.put("key1", "value1");
        modifiableMap.put("key2", "value2");
        modifiableMap.put("key3", "value3");
        Map<String, String> map = Collections.unmodifiableMap(modifiableMap);
        System.out.println(map);
    }
}

注意:Collections.unmodifiableMap()创建的映射是不可变的,即不能添加、删除或替换键值对。

方法二(双括号初始化)不推荐在多线程环境中使用的原因主要与Java的匿名内部类和实例变量的初始化顺序有关。以下是详细解释:

1. 匿名内部类的实例变量#

当你使用双括号初始化时,实际上是在创建一个匿名内部类的实例。这个匿名内部类会捕获外部类的实例变量(如果有的话),并且在内部类中创建一个新的HashMap实例。

2. 实例变量的初始化顺序#

在Java中,实例变量的初始化顺序是先执行父类的构造方法,然后是子类的成员变量初始化,最后是子类的构造方法。当你在一个类的构造方法中使用双括号初始化时,这个初始化过程可能会在这个类的实例完全构造完成之前开始。

3. 多线程环境中的问题#

在多线程环境中,如果多个线程同时访问同一个尚未完全构造的对象,就可能导致以下问题:

  • 可见性:一个线程对共享变量的修改可能对其他线程不可见。
  • 竞态条件:多个线程同时读取和修改共享数据,导致不可预测的结果。 由于双括号初始化涉及到捕获外部类的实例变量,如果在多线程环境中使用,就可能遇到上述问题。特别是当这个HashMap被多个线程共享时,就可能导致数据不一致或其他并发问题。

4. 示例#

以下是一个简化的示例,说明为什么双括号初始化在多线程环境中可能有问题:

public class SomeClass {
    private Map<String, String> map = new HashMap<String, String>() {{
        put("key1", "value1");
        put("key2", "value2");
    }};
    public SomeClass() {
        // 构造方法中的其他初始化代码
    }
    public Map<String, String> getMap() {
        return map;
    }
}

在这个示例中,如果多个线程在SomeClass的实例完全构造完成之前就调用getMap()方法,它们可能会看到一个未完全初始化的map

为了避免这些潜在的问题,推荐在多线程环境中使用更明确和线程安全的方法来初始化HashMap,比如方法一(使用匿名内部类但不使用双括号)或其他线程安全的集合初始化方法。这样可以确保在多线程环境中对共享数据的访问是安全的。

不可变集合#

不可变集合是一个长度不可变,内容也无法修改的集合

使用场景#

如果某个数据不能被修改,把它防御性地拷贝到不可变集合中是个很好的实践。 当集合对象被不可信的库调用时,不可变形式是安全的。

不可变的list集合#

List<String> list = List.of("张三", "李四", "王五", "赵六");
System.out.println("---------------------------");
for (String s : list) {
    System.out.println(s);
}
System.out.println("---------------------------");
Iterator<String> it = list.iterator();
while(it.hasNext()){
    String s = it.next();
    System.out.println(s);
}
System.out.println("---------------------------");
for (int i = 0; i < list.size(); i++) {
    String s = list.get(i);
    System.out.println(s);
}
System.out.println("---------------------------");
//list.remove("李四");
//list.add("aaa");
list.set(0,"aaa");

不可变的Set集合#

Set<String> set = Set.of("张三", "张三", "李四", "王五", "赵六");
for (String s : set) {
    System.out.println(s);
}
System.out.println("-----------------------");
Iterator<String> it = set.iterator();
while(it.hasNext()){
    String s = it.next();
    System.out.println(s);
}
System.out.println("-----------------------");
//set.remove("王五");

不可变的Map集合#

键值对个数小于等于10#

public class ImmutableDemo3 {
    public static void main(String[] args) {
       /*
        创建Map的不可变集合
            细节1:
                键是不能重复的
            细节2:
                Map里面的of方法,参数是有上限的,最多只能传递20个参数,10个键值对
            细节3:
                如果我们要传递多个键值对对象,数量大于10个,在Map接口中还有一个方法
        */

        //一旦创建完毕之后,是无法进行修改的,在下面的代码中,只能进行查询操作
        Map<String, String> map = Map.of("张三", "南京", "张三", "北京", "王五", "上海", "赵六", "广州", "孙七", "深圳", "周八", "杭州", "吴九", "宁波", "郑十", "苏州", "刘一", "无锡", "陈二", "嘉兴");
        Set<String> keys = map.keySet();
        for (String key : keys) {
            String value = map.get(key);
            System.out.println(key + "=" + value);
        }
        System.out.println("--------------------------");
        Set<Map.Entry<String, String>> entries = map.entrySet();
        for (Map.Entry<String, String> entry : entries) {
            String key = entry.getKey();
            String value = entry.getValue();
            System.out.println(key + "=" + value);
        }
        System.out.println("--------------------------");
    }
}

键值对个数大于10#

public class ImmutableDemo4 {
    public static void main(String[] args) {

        /*
            创建Map的不可变集合,键值对的数量超过10个
        */

        //1.创建一个普通的Map集合
        HashMap<String, String> hm = new HashMap<>();
        hm.put("张三", "南京");
        hm.put("李四", "北京");
        hm.put("王五", "上海");
        hm.put("赵六", "北京");
        hm.put("孙七", "深圳");
        hm.put("周八", "杭州");
        hm.put("吴九", "宁波");
        hm.put("郑十", "苏州");
        hm.put("刘一", "无锡");
        hm.put("陈二", "嘉兴");
        hm.put("aaa", "111");

        //2.利用上面的数据来获取一个不可变的集合
/*
        //获取到所有的键值对对象(Entry对象)
        Set<Map.Entry<String, String>> entries = hm.entrySet();
        //把entries变成一个数组
        Map.Entry[] arr1 = new Map.Entry[0];
        //toArray方法在底层会比较集合的长度跟数组的长度两者的大小
        //如果集合的长度 > 数组的长度 :数据在数组中放不下,此时会根据实际数据的个数,重新创建数组
        //如果集合的长度 <= 数组的长度:数据在数组中放的下,此时不会创建新的数组,而是直接用
        Map.Entry[] arr2 = entries.toArray(arr1);
        //不可变的map集合
        Map map = Map.ofEntries(arr2);
        map.put("bbb","222");*/


        //Map<Object, Object> map = Map.ofEntries(hm.entrySet().toArray(new Map.Entry[0]));

        Map<String, String> map = Map.copyOf(hm);
        map.put("bbb","222");
    }
}
java学习笔记(6)
http://zaoweiceng.github.io/posts/java/java学习笔记_6/
作者
静惜
发布于
2025-03-12
许可协议
CC BY-NC-SA 4.0