Enum

In Java, an enum (short for “enumeration”) is used to define a collection of constants, such as days of the week or months of the year. An enum provides a type-safe way to handle constant values, and it can be used in a switch statement or for comparison using ==. Compared to a final constant in Java, an enum can contain fields, methods, and constructors. The values() method is used to retrive all the constants in an enum, and the valueOf() method is used to convert a string into an enum constant.

SHOW CODE
enum Day {
    SUNDAY(1),
    MONDAY(2),
    TUESDAY(3),
    WEDNESDA(4),
    THURSDAY(5),
    FRIDAY(6),
    SATURDAY(7);

    private final int dayNum;

    Day(int dayNum) {
        this.dayNum = dayNum;
    }

    public int getDayNum() {
        return dayNum;
    }

    public static Day fromNumber(int num) {
        for (Day day : Day.values()) {
            if (day.getDayNum() == num) {
                return day;
            }
        }
        throw new IllegalArgumentException("Invalid day number: " + num);
    }

    @Override
    public String toString() {
        return name() + "(" + dayNum + ")";
    }
}

public class Solution {
    public static void main(String[] args) {
        for (Day d : Day.values()) {
            System.out.print(d + "\t");
        }

        System.out.println("\n" + Day.fromNumber(1));
//        System.out.println(Day.fromNumber(8));

        // Converts a string to enum constant
        System.out.println(Day.valueOf("MONDAY"));

    }
}

Generics

In Java, Generics is a mechanism that allows writing classes, interfaces, and methods with type parameters, often represented by T, E, K, and V, and allows calling them with different type parameters. It provides strong type safety checks at compile time, enhances code reusability, and eliminates the need for casting.

Basic Generics

SHOW CODE
interface Pair {
    K getKey();
    V getValue();
}

class SimplePair<K,V> implements Pair<K,V> {

    private final K key;
    private final V value;

    public SimplePair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    @Override
    public K getKey() {
        return key;
    }

    @Override
    public V getValue() {
        return value;
    }

    @Override
    public String toString() {
        return "(" + key + "," + value + ")";
    }
}

public class Main {
    public static void main(String[] args) {
        SimplePair<String, Integer> pair = new SimplePair<>("Signal", 27);
        System.out.println("pair = " + pair);
        String key = pair.getKey();
        System.out.println("key = " + key);
        Integer value = pair.getValue();
        System.out.println("value = " + value);
    }
}

Bounded Type Parameters

Bounded Type Parameters in Java allow restricting the range of types when writing generic code. It can be categorized into four types: Upper Bounded Type, Lower Bounded Type, Unbounded Type, and Multiple Bounds.


Upper Bounded Type

In Java, Upper Bounded Wildcards are used to define upper bounded types. The syntax for an upper bounded wildcard is <T extends SomeClassOrInterface>, which indicates that the argument type T must be a subclass of SomeClassOrInterface or SomeClassOrInterface itself.

SHOW CODE
import java.util.List;

public class Main {
    // Method that accepts a list of elements that are of type Number or its subclasses
    public static void printNumbers(List<? extends Number> list) {
        for (Number number : list) {
            System.out.println(number);
        }
    }

    public static void main(String[] args) {
        List<Integer> intList = List.of(1, 2, 3, 4);
        List<Double> doubleList = List.of(1.1, 2.2, 3.3);

        printNumbers(intList);   // Valid, Integer is a subclass of Number
        printNumbers(doubleList); // Valid, Double is a subclass of Number
    }
}

Lower Bounded Type

In Java, Lower Bounded Wildcards are used to define lower bounded types. The syntax for a lower bounded wildcard is <T super SomeClassOrInterface>, which indicates that the argument type T must be a super class of SomeClassOrInterface or SomeClassOrInterface itself.

SHOW CODE
import java.util.ArrayList;
import java.util.List;

// Custom class Person
class Person {
    String name;

    Person(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "'}";
    }
}

// Subclass Employee extends Person
class Employee extends Person {
    double salary;

    Employee(String name, double salary) {
        super(name);
        this.salary = salary;
    }

    @Override
    public String toString() {
        return "Employee{name='" + name + "', salary=" + salary + "}";
    }
}

public class Main {
    // Method that accepts a list of Person or its superclasses (e.g., Object)
    // This method can add Employee objects to the list.
    public static void addEmployees(List<? super Employee> list) {
        list.add(new Employee("Alice", 50000));
        list.add(new Employee("Bob", 60000));
        list.add(new Employee("Charlie", 70000));
    }

    public static void main(String[] args) {
        // Create a list of Person, which is a superclass of Employee
        List<Person> personList = new ArrayList<>();

        // Add Employee objects to the personList using the addEmployees method
        addEmployees(personList);

        // Print the list to see the added Employee objects
        for (Person person : personList) {
            System.out.println(person);
        }
    }
}
SHOW OUTPUT
Employee{name='Alice', salary=50000.0}
Employee{name='Bob', salary=60000.0}
Employee{name='Charlie', salary=70000.0}

Unbounded Type

In Java, the Unbounded Type is represented by using the unbounded wildcard ?, which indicates that no restrictions are placed on the type of an argument passed to the method. It is commonly used when passing a collection of elements to a method where the specific type of the elements is not important. With an unbounded wildcard, elements can be read but cannot be added to the collection (except null) inside the methods.

SHOW CODE
import java.util.ArrayList;
import java.util.List;

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person(" + name + ", " + age + ")";
    }
}

public class Main {

    public static void printList(List<?> list) {
        // Iterate over the list and print each element
        list.add(null);
//        list.add(1);
        for (Object element : list) {
            System.out.println(element);
        }
    }

    public static void main(String[] args) {
        // Create lists of different types
        List<String> stringList = new ArrayList<>();
        stringList.add("Hello");
        stringList.add("World");

        List<Integer> intList = new ArrayList<>();
        intList.add(10);
        intList.add(20);

        List<Object> objectList = new ArrayList<>();
        objectList.add("Some String");
        objectList.add(100);
        objectList.add(1.1);
        objectList.add('a');
        objectList.add(true);
        objectList.add(new Person("Signal", 18));
        objectList.add(null);

        // Call the printList method for each list
        System.out.println("String List:");
        printList(stringList);  // Works with List<String>

        System.out.println("\nInteger List:");
        printList(intList);     // Works with List<Integer>

        System.out.println("\nObject List:");
        printList(objectList);  // Works with List<Object>
    }
}
SHOW OUTPUT
String List:
Hello
World

Integer List:
10
20

Object List:
Some String
100
1.1
a
true
Person(Signal, 18)
null
null

Multiple Bounds

In Java, a Multiple Bounds constraint allows the argument type T passed to a method to extend multiple types or implement multiple interfaces. This is commonly achieved using the & operator. The syntax for multiple bounds is <T extends classType & Interface1 & Interface2>, indicating that the argument type T must extend the class classType and implement both Interface1 and Interface2.

SHOW CODE
// Interface 1
interface CanFly {
    void fly();
}

// Interface 2
interface CanSwim {
    void swim();
}

// A base class for animals
class Animal {
    String name;

    Animal(String name) {
        this.name = name;
    }
}

// Class that implements both interfaces
class Duck extends Animal implements CanFly, CanSwim {
    Duck(String name) {
        super(name);
    }

    @Override
    public void fly() {
        System.out.println(name + " is flying.");
    }

    @Override
    public void swim() {
        System.out.println(name + " is swimming.");
    }
}

// A class that uses multiple bounds in its type parameter
class AnimalAction<T extends Animal & CanFly & CanSwim> {
    T animal;

    AnimalAction(T animal) {
        this.animal = animal;
    }

    void performActions() {
        animal.fly();
        animal.swim();
    }
}

public class Main {
    public static void main(String[] args) {
        // Create a Duck object
        Duck duck = new Duck("Dodo");

        // Create an AnimalAction object for Duck, which extends Animal and implements CanFly and CanSwim
        AnimalAction<Duck> action = new AnimalAction<>(duck);

        // Perform actions on the Duck
        action.performActions();
    }
}

Reflection

Reflection in Java is a feature that allows inspecting and manipulating the properties or behaviors of classes, methods, fields, and constructors at runtime. It is commonly used to examine class information, access private methods and fields, invoke methods dynamically, and create objects dynamically using the Java Reflection API. The Java Reflection API is provided by the java.lang.reflect package and the java.lang.Class class. Some of the key classes and methods in this API include:

  1. Class: Represents the class of an object and provides methods like getName(), getDeclaredMethods(), getDeclaredFields(), and others to examine the class’s metadata.
  2. Method: Represents a method of a class and allows invoking it dynamically using methods like invoke().
  3. Field: Represents a field (variable) in a class and allows accessing or modifying its value dynamically.
  4. Constructor: Represents a constructor in a class and allows creating new instances dynamically using methods like newInstance().
SHOW CODE
package dev.signalyu.warmup;

import java.lang.reflect.*;

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    private void sayHello() {
        System.out.println("Hello, " + name);
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class Main {
    public static void main(String[] args) throws Exception {
        // Create a new Person object
        Person person = new Person("John", 30);

        // Get the Class object associated with the Person class
        Class clazz = person.getClass();

        // Get class name
        System.out.println("Class name: " + clazz.getName());

        // Get declared methods
        Method[] methods = clazz.getDeclaredMethods();
        for (Method method : methods) {
            System.out.println("Method: " + method.getName());
        }

        // Get declared fields
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            System.out.println("Field: " + field.getName());
        }

        // Accessing a private field via reflection
        Field field = clazz.getDeclaredField("name");
        field.setAccessible(true); // Make the private field accessible
        System.out.printf("Private field: 'name'=%s\n", field.get(person));

        // Get the 'sayHello' method
        Method method = clazz.getDeclaredMethod("sayHello");
        // Make the private method accessible
        method.setAccessible(true);
        // Invoke the method on the 'person' object
        method.invoke(person);

        // Dynamically load the Person class
        Class clazz2 = Class.forName("dev.signalyu.warmup.Person");

        // Get the constructor that takes a String argument
        Constructor constructor = clazz2.getConstructor(String.class, int.class);

        // Create an instance of Person using reflection
        Person Signal = (Person) constructor.newInstance("Signal", 18);

        System.out.println("Signal = " + Signal);
    }
}
SHOW OUTPUT
Class name: dev.signalyu.warmup.Person
Method: getName
Method: toString
Method: setName
Method: sayHello
Method: getAge
Field: name
Field: age
Private field: 'name'=John
Hello, John
Signal = Person{name='Signal', age=18}


Annotations

Annotations in Java are a form of metadata used to provide additional information that does not directly affect the execution of the code. They are commonly used to convey instructions to the compiler (such as detecting errors or supressing warnings) or to support runtime processing. Annotations are prefixed with the @ symbol and can be applied to classes, methods, firlds, and other program elements.

Common meta annotations, such as @Target adn @Retension, are used to specify how and where an annotation can be applied. The @Target annotation defines the valid program elements for an annotation, including:

  • ElementType.TYPE: The annotation can be applied to a class or interface.
  • ElementType.METHOD: The annotation can be applied to a method.
  • ElementType.FIELD: The annotation can be applied to a field.

The @Retention annotation specifies the lifespan of the annotation. Foe example, using @Retention(RetentionPolicy).RUNTIME indicates that the annotation is available at runtime for reflection.

To customize an annotation in Java, the first step is to define its basic structure using the @interface keyword. Elements (parameters) can be included in the annotation, with or without default values, depending on the requirements.

Next, meta-annotation such as @Target and @Retention are added to specify where the annotation can be applied and how long it will be retained.

Once the annotation is defined, it can be applied according to the specified @Target (e.g., on methods, classes, or fields).

The final step is to define the logic for processing the annotation, typically achieved through reflection at runtime.


SHOW CODE: Login Interception
@Getter
public enum ResultCodeEnum {
    LOGIN_AUTH(208, "Not logged in");

    private final int code;
    private final String message;

    ResultCodeEnum(int code, String message) {
        this.code = code;
        this.message = message;
    }
}

public class RedisConstant {
    /**
     * The prefix for user login keys in Redis.
     */
    public static final String USER_LOGIN_KEY_PREFIX = "user:login:";
}

public class AuthContextHolder {

    private static final ThreadLocal<Long> userId = new ThreadLocal<>();

    /**
     * Sets the user ID in the thread-local context.
     *
     * @param _userId The user ID to set.
     */
    public static void setUserId(Long _userId) {
        userId.set(_userId);
    }

    /**
     * Gets the user ID from the thread-local context.
     *
     * @return The current user ID.
     */
    public static Long getUserId() {
        return userId.get();
    }

    /**
     * Removes the user ID from the thread-local context.
     */
    public static void removeUserId() {
        userId.remove();
    }
}

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface AppLogin {
    /**
     * Indicates if login is required for the annotated method.
     * Default is true.
     */
    boolean required() default true;
}

@Aspect
@Component
public class AppLoginAspect {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * Intercepts methods annotated with @AppLogin to enforce login validation.
     *
     * @param joinPoint The proceeding join point for the intercepted method.
     * @param appLogin The AppLogin annotation instance.
     * @return The result of the intercepted method execution.
     * @throws Throwable If an error occurs during method execution.
     */
    @SneakyThrows
    @Around("execution(* com.atApp.tingshu.*.api.*.*(..)) && @annotation(appLogin)")
    public Object loginAspect(ProceedingJoinPoint joinPoint, AppLogin appLogin) {
        // Extract the token from the request header
        String token = getTokenFromRequest();

        // Check login status in Redis
        String loginKey = RedisConstant.USER_LOGIN_KEY_PREFIX + token;
        UserInfoVo userInfoVo = (UserInfoVo) redisTemplate.opsForValue().get(loginKey);

        // If login is required and user is not authenticated, throw exception
        if (appLogin.required() && userInfoVo == null) {
            throw new AppException(ResultCodeEnum.LOGIN_AUTH);
        }

        // If user is authenticated, set the user ID in the thread-local context
        if (userInfoVo != null) {
            AuthContextHolder.setUserId(userInfoVo.getId());
        }

        try {
            // Proceed with the method execution
            return joinPoint.proceed();
        } finally {
            // Ensure that user ID is cleared after the method execution
            AuthContextHolder.removeUserId();
        }
    }

    /**
     * Extracts the token from the HTTP request header.
     *
     * @return The token string.
     */
    private String getTokenFromRequest() {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
        HttpServletRequest request = servletRequestAttributes.getRequest();
        return request.getHeader("token");
    }
}

Serialization & Deserialization

In Java, Serialization and Deserialzation are processes used to convert objects into a byte stream and restore them back into objects, respectively. These processes are essential for storing objects in files, transmitting them over a network, or preserving their state.

Serialization involves converting an object into a byte stream. To serialize an object, its class must implement the java.io.Serializable interface, which is a marker interface without any methods. The ObjectOutputStream class is used to write the serialized object to a file or another output stream. A serialVersionUID is automatically generated if not explicitly defined, but it is recommended to provide one manually to ensure compatibility during deserialization. Fields marked as transcient are not serialized. Additionally, any objects regerenced by the serialized object are also serialized, provided they implement the Serializable interface.

Deserialization is the process of reconstructing a serialized object back into a Java object. For deserialization to succeed, the class must be available in the classpath. This process is performed using the ObjectInputStream class, which reads the byte stream and restores the object to its original state.

SHOW CODE
import java.io.*;

// Define a Serializable class
class Person implements Serializable {
    private static final long serialVersionUID = 1L; // Ensures class compatibility during deserialization

    private String name;
    private int age;

    // Transient field will not be serialized
    private transient String password;

    public Person(String name, int age, String password) {
        this.name = name;
        this.age = age;
        this.password = password;
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + ", password='" + password + "'}";
    }
}

public class Main {
    public static void main(String[] args) {
        Person person = new Person("Signal Yu", 18, "securePassword");

        // Serialize the object
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.ser"))) {
            oos.writeObject(person);
            System.out.println("Object serialized successfully.");
        } catch (IOException e) {
            e.printStackTrace();
        }

        // Deserialize the object
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.ser"))) {
            Person deserializedPerson = (Person) ois.readObject();
            System.out.println("Deserialized object: " + deserializedPerson);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
SHOW OUTPUT
Object serialized successfully.
Deserialized object: Person{name='Signal Yu', age=18, password='null'}

Lambda Expression

Runnable

SHOW CODE
public class Test {
    public static void main(String[] args) {

        // Java 8 之前
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("Inside Runnable 1");
            }
        };
        new Thread(runnable).start();

        // Java 8 - Lambda 语法
        Runnable runnableLambda = () -> {System.out.println("Inside Runnable 2");};
        new Thread(runnableLambda).start();

        new Thread(() -> System.out.println("Inside Runnable 3")).start();
    }
}

Comparator

SHOW CODE
public class Test {
    public static void main(String[] args) {
        // JAVA 8 之前
        Comparator<Integer> comparator = new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                /**
                 * o1 < o2 -> -1
                 * o1 == o2 -> 0
                 * o1 > o2 -> 1
                 */
                return o1.compareTo(o2);
            }
        };
        System.out.println(comparator.compare(1, 2)); // -1

        /**
         * JAVA 8
         */
        // Comparator<Integer> comparatorLambda1 = (Integer a, Integer b) -> a.compareTo(b);
        Comparator<Integer> comparatorLambda1 = Comparator.naturalOrder();
        System.out.println(comparatorLambda1.compare(1, 2)); // -1

        // Comparator<Integer> comparatorLambda2 = (a, b) -> b.compareTo(a);
        Comparator<Integer> comparatorLambda2 = Comparator.reverseOrder();
        System.out.println(comparatorLambda2.compare(1, 2)); // 1

        int[] nums = new int[]{2, 1, 4, 3};
        System.out.println("nums = " + Arrays.toString(nums));
        Arrays.sort(nums); // 默认为升序排序

        // 倒序排序
        Integer[] newNums = Arrays.stream(nums).boxed().toArray(Integer[]::new);
        Arrays.sort(newNums, Comparator.reverseOrder());
    }
}

Local Variables in Lambda Expression

SHOW CODE
public class Test {
    public static void main(String[] args) {
        /**
         * 1. The variable used in a lambda expression must be effectively final,
         * meaning its value cannot change after it has been assigned.
         * 2. A lambda captures the values of local variables, not the variables
         * themselves.
         * 3. Local variables in a lambda cannot have the same name as variables
         * in the enclosing scope to avoid shadowing.
         */
        int num = 10; // effectively final
        Runnable r = () -> System.out.println(num);
        r.run();

        // Error: Variable num is already defined in the scope
//        Runnable r = () -> {
//            int num = 20; // Compile-time error
//            System.out.println(num);
//        };
    }
}

Functional Interface

Consumer

SHOW CODE
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

public class Test {
    public static void main(String[] args) {
        List<String> items = Arrays.asList("One", "Two", "Three");

        /**
         * Consumer 接收一个参数并执行一些操作,不返回结果
         * Consumer 有 accept 和 andThen 两个方法
         * accept 用于接收参数,andThen 用于链接多个 Consumer
         */
        // 示例1:定义一个 Consumer,它将打印传入的字符串
        Consumer<String> printConsumer = s -> System.out.println("打印消息: " + s);
        printConsumer.accept("Hello, World!"); // 输出: 打印消息: Hello, World!

        // 示例 2:Consumer 链式操作
        Consumer<String> printItem = item -> System.out.print("打印: " + item + "\t");
        Consumer<String> printLength = item -> System.out.println("长度: " + item.length());
        Consumer<String> combined = printItem.andThen(printLength); // 将两个 Consumer 组合在一起
        // 使用 forEach 遍历集合并执行组合操作
        items.forEach(combined);
//        items.forEach(item -> {
//            System.out.print("打印: " + item + "\t");
//            System.out.println("长度: " + item.length());
//        });
    }
}

BiConsumer

SHOW CODE
import java.util.Arrays;
import java.util.List;
import java.util.function.BiConsumer;

// 定义 Student 记录类,包含学生的姓名和活动列表
record Student(String name, List<String> activities) {
}

//class Student {
//    private final String name;
//    private final  List<String> activities;
//
//    public Student(String name, List<String> activities) {
//        this.name = name;
//        this.activities = activities;
//    }
//
//    public String getName() {
//        return name;
//    }
//
//    public List<String> getActivities() {
//        return activities;
//    }
//}

class StudentDataBase {
    // 提供学生数据,返回一个包含多个学生的列表
    public static List<Student> getAllStudents() {
        return Arrays.asList(
                new Student("Alice", Arrays.asList("Swimming", "Basketball")),
                new Student("Bob", Arrays.asList("Cycling", "Chess")),
                new Student("Charlie", Arrays.asList("Running", "Reading"))
        );
    }
}

class Test {
    public static void main(String[] args) {
        /**
         * BiConsumer 适用于对两个参数执行操作,但不需要返回结果的场景。
         */
        
        // BiConsumer 接口用于处理学生姓名和活动列表
        BiConsumer<<String, List<String>> studentBiConsumer = (name, activities) ->
                System.out.println(name + " : " + activities);

        // 获取学生列表
        List<Student> students = StudentDataBase.getAllStudents();

        // 使用 forEach 遍历每个学生对象并输出其姓名和活动列表
        students.forEach(student ->
                studentBiConsumer.accept(student.name(), student.activities()));
    }
}

Predicate

SHOW CODE
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

class Test {
    public static void main(String[] args) {
        /*
           Predicate 用于测试输入对象是否满足某种条件,常用于过滤、条件判断等场景
           Predicate 提供了 and, or 和 negate 三个默认方法
           Predicate 提供了 isEqual 和 not (Java 11) 两个静态方法
         */

        // 示例 1:
        Predicate<Integer> isEven = num -> num % 2 == 0;
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
        List<Integer> evenNumbers = numbers.stream()
                .filter(isEven)
                .toList();
        System.out.println("偶数: " + evenNumbers); // 输出: 偶数: [2, 4, 6]

        // 示例 2:
        Predicate<Integer> isOdd = num -> num % 2 == 0;
        Predicate<Integer> isGreaterThanFive = num -> num > 5;
        // 使用 and() 方法组合两个条件
        Predicate<Integer> isOddAndGreaterThanFive = isOdd.and(isGreaterThanFive);
        System.out.println(isOddAndGreaterThanFive.test(7)); // 输出: true
        System.out.println(isOddAndGreaterThanFive.test(3)); // 输出: false

        // 示例 3
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Alice");

        // 使用 isEqual 和 not 来筛选出不等于 "Alice" 的名字
        List<String> filteredNames = names.stream()
                                          .filter(Predicate.not(Predicate.isEqual("Alice")))
                                          .toList();
        System.out.println(filteredNames); // Output: [Bob, Charlie]
    }
}

BiPredicate

SHOW CODE
import java.util.function.BiPredicate;

class Test {
    public static void main(String[] args) {
        // BiPredicate 适合需要对两个参数进行测试或判断的情况

        // 示例 1        
        BiPredicate<Integer, Integer> isSumGreaterThanTen = (a, b) -> (a + b) > 10;
        BiPredicate<Integer, Integer> isProductEven = (a, b) -> (a * b) % 2 == 0;

        BiPredicate<Integer, Integer> combined = isSumGreaterThanTen.and(isProductEven);
        System.out.println(combined.test(5, 6)); // 输出: true,因为和大于10且乘积为偶数
        System.out.println(combined.test(5, 5)); // 输出: false,因为乘积为奇数
    }
}

Function

SHOW CODE
import java.util.function.Function;

class Test {
    public static void main(String[] args) {
        /**
         *  Function 接受一个参数并返回一个结果
         *  apply(T t):对给定的参数执行函数操作并返回结果
         *  andThen():在当前函数之后执行另一个函数
         *  compose():在当前函数之前执行另一个函数
         */

        // 示例 1
        Function<Integer, Integer> squareFunction = x -> x * x;
        Integer result = squareFunction.apply(5);
        System.out.println("5 的平方是: " + result);

        // 示例 2
        Function<Integer, Integer> multiplyBy2 = x -> x * 2;
        Function<Integer, Integer> add3 = x -> x + 3;
        // 使用 andThen:先乘以 2,再加 3
        Integer result1 = multiplyBy2.andThen(add3).apply(5);
        System.out.println("(5 * 2) + 3 的结果是: " + result1);
        // 使用 compose:先加 3,再乘以 2
        Integer result2 = multiplyBy2.compose(add3).apply(5);
        System.out.println("(5 + 3) * 2 的结果是: " + result2);
    }
}

BiFunction

SHOW CODE
import java.util.function.BiFunction;

class Test {
    public static void main(String[] args) {
        // BiFunction takes two arguments and returns a result
        BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
        int result = sum.apply(5, 3);  // Returns 8
        System.out.println("Result: " + result);
    }
}

Method Reference

SHOW CODE
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;

public class Test {
    public static void main(String[] args) {
        /**
         * Method reference is a shortcut for writing the lambda function
         */

        // 示例 1
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
        // 使用 Lambda 表达式
        names.stream().map(name -> name.toUpperCase()).forEach(name -> System.out.println(name));
        // 使用Method Reference
        names.stream().map(String::toUpperCase).forEach(System.out::println);

        /**
         * 构造器引用是Method Reference的特例
         * 用法如下:
         *  () -> new Person() <--> Person::new;
         *  (name) -> new Person() <--> Person::new;
         */
        // 示例 2
        // Using () -> new Person()
        Supplier<Person> personSupplier = () -> new Person();
        Person p1 = personSupplier.get();
        System.out.println(p1); // Output: Person{name='Default'}
        // Using Person::new
        Supplier<Person> personSupplierRef = Person::new;
        Person p2 = personSupplierRef.get();
        System.out.println(p2); // Output: Person{name='Default'}

        // 示例 3
        // Using (name) -> new Person(name)
        Function<String, Person> personFunction = (name) -> new Person(name);
        Person p3 = personFunction.apply("Alice");
        System.out.println(p3); // Output: Person{name='Alice'}
        // Using Person::new
        Function<String, Person> personFunctionRef = Person::new;
        Person p4 = personFunctionRef.apply("Bob");
        System.out.println(p4); // Output: Person{name='Bob'}
    }
}

class Person {
    private String name;

    // Default constructor
    public Person() {
        this.name = "Default";
    }

    // Constructor with a parameter
    public Person(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "'}";
    }
}

Stream API

Introduction to Stream API

A Stream is a sequence of elements designed for processing in a functional and sequential manner. Stream operations are lazy, meaning they are not executed until a terminal operation (e.g., collect, forEach, reduce, count) is invoked. Streams are immutable, allowing access to elements without modifying the source. Java provides two types of streams: stream (for sequential processing) and parallel stream (for concurrent processing).

Collections V.S. Stream

  1. In Java, Collections are data structures designed for storing and manipulating data, whereas Streams are a sequence of elements which enable functional-style operations on data.
  2. Collections allow data modification, whereas Streams provide read-only access to elements.
  3. Operations on Collections are executed eagerly, while Streams operations are lazy and only executed when a terminal operation is invoked.
  4. Collections can be reused multiple times, but Streams are single-use and cannot be reused after processing.

Debugging Streams: the peek() method can be used to inspect the intermediate results during stream operations.

Arrays.asList("hello", "hi").stream()
                .peek(System.out::println)
                .filter(s -> s.length() > 2)
                .peek(s -> System.out.println("Filtered: " + s))
                .map(String::toUpperCase)
                .peek(s -> System.out.println("Mapped: " + s))
                .collect(Collectors.toList());

hello
Filtered: hello
Mapped: HELLO
hi

flatMap

The flatMap method is used to transform each element in a stream into another stream and then flatten the resulting streams into a single stream.

SHOW CODE
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Test {
    public static void main(String[] args) {
        List<List<String> nestedList = Arrays.asList(
                Arrays.asList("A", "B"),
                Arrays.asList("C", "D"),
                Arrays.asList("E", "F")
        );

        List<String> flattenedList = nestedList.stream()
                .flatMap(List::stream) // Flatten the nested lists
                .map(String::toLowerCase) // Convert each string to lowercase
                .collect(Collectors.toList());

        System.out.println(flattenedList); // Output: [a, b, c, d, e, f]
    }
}

The map method transforms each element into another type, while the flatMap method transforms and flaten streams into a single stream.


distinct(), count(), sorted()

The distinct() method eliminates duplicate elements from the stream. The count() method calculates the total number of elements in the stream. The sorted() method sorts the elements of the stream.

SHOW CODE
List<String> names = Arrays.asList("Alice", "Bob", "Alice", "Charlie");
long count = names.stream()
                  .distinct() // Optional: Count unique elements
                  .count();
System.out.println(count); // Output: 3

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<String> sortedByLength = names.stream()
                                   .sorted(Comparator.comparingInt(String::length))
                                   .collect(Collectors.toList());
System.out.println(sortedByLength); // Output: [Bob, Alice, Charlie]

reduce()

The reduce() method in Java 8 Streams is a terminal operation used to combine elements in a stream into a single result by repeatedly applying a binary operator.

SHOW CODE
// Summing numbers
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().reduce(0, Integer::sum);
System.out.println(sum); // Output: 15

// Finding maximum values
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int max = numbers.stream().reduce(Integer.MIN_VALUE, Integer::max);
System.out.println(max); // Output: 5

// Concatenating Strings
List<String> words = Arrays.asList("Java", "Stream", "Reduce");
String concatenated = words.stream().reduce("", String::concat);
System.out.println(concatenated); // Output: JavaStreamReduce

// Product of numbers
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
int product = numbers.stream().reduce(1, (a, b) -> a * b);
System.out.println(product); // Output: 24

limit(), skip()

The limit() method retrieves a specified number of elements from a stream. The skip() method discards the first specified number of elements in a stream.

SHOW CODE
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve");
List<String> paginatedNames = names.stream()
                                   .skip(2)   // Skip the first 2 elements
                                   .limit(2)  // Take the next 2 elements
                                   .collect(Collectors.toList());
System.out.println(paginatedNames); // Output: [Charlie, David]

allMatch(), anyMatch(), noneMatch()

The allMatch() method verifies if all elements in a stream satisfy the given predicate. The anyMatch() method determines if any element in the stream matches the specified predicate. The noneMatch() method confirms if no element in the stream matches the specified predicate.

SHOW CODE
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

// Check if all names are longer than 3 characters
boolean allLongNames = names.stream().allMatch(name -> name.length() > 3);
System.out.println(allLongNames); // Output: true

// Check if any name starts with 'A'
boolean anyStartsWithA = names.stream().anyMatch(name -> name.startsWith("A"));
System.out.println(anyStartsWithA); // Output: true

// Check if no name is shorter than 3 characters
boolean noneShortNames = names.stream().noneMatch(name -> name.length() < 3);
System.out.println(noneShortNames); // Output: true

findAny(), findFirst()

The findAny() method retrieves an arbitrary element from a stream, particularly useful for parallel processing. The findFirst() method fetches the first element from a stream based on its encounter order.

SHOW CODE
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Optional<String> anyName = names.stream().findAny();
System.out.println(anyName.orElse("No names found")); // Output: Alice (or any other element)

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Optional<String> firstName = names.stream().findFirst();
System.out.println(firstName.orElse("No names found")); // Output: Alice

Factory Methods: Of(), generate(), iterate()

Stream.of() is used to create a stream from a fixed set of elements.

SHOW CODE
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("Apple", "Banana", "Cherry");
        stream.forEach(System.out::println);
    }
}

Stream.generate() method is used to create an infinite stream of elements generated by a Supplier.

SHOW CODE
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        Stream<String> infiniteStream = Stream.generate(() -> "Hello");
        infiniteStream.limit(5).forEach(System.out::println);
    }
}

Stream.iterate() is used to create an infinite stream by applying a function repeatedly to a seed value.

SHOW CODE
import java.util.Random;

Stream<Integer> randomNumbers = Stream.generate(() -> new Random().nextInt(100));
randomNumbers.limit(5).forEach(System.out::println);

When working with infinite streams, such as those created by Stream.generate() or Stream.iterate(), always use operations like limit() to prevent infinite processing.

Terminal Operations: joining(), counting(), mapping()

The joining method is a terminal operation used to concatenate the elements of a stream into a single String. By default, the elements are joined without a delimiter, but a custom delimiter, prefix, and suffix can also be specified.

SHOW CODE
import java.util.*;
import java.util.stream.Collectors;

public class JoiningExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

        // Simple joining
        String result1 = names.stream().collect(Collectors.joining());
        System.out.println(result1); // AliceBobCharlie

        // Joining with a delimiter
        String result2 = names.stream().collect(Collectors.joining(", "));
        System.out.println(result2); // Alice, Bob, Charlie

        // Joining with delimiter, prefix, and suffix
        String result3 = names.stream().collect(Collectors.joining(", ", "[", "]"));
        System.out.println(result3); // [Alice, Bob, Charlie]
    }
}

The counting() method is a terminal operation used to count the number of elements in a stream and return the count as a long.

SHOW CODE
import java.util.*;
import java.util.stream.Collectors;

public class CountingExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        // Counting the elements in the stream
        long count = numbers.stream().collect(Collectors.counting());
        System.out.println(count); // 5
    }
}

The mapping() method is an intermediate collector that applies a function to the elements of a stream before passing the transformed elements to another collector for final processing.

SHOW CODE
import java.util.*;
import java.util.stream.Collectors;

public class MappingExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

        // Convert names to uppercase and join them
        String result = names.stream()
                .collect(Collectors.mapping(String::toUpperCase, Collectors.joining(", ")));
        System.out.println(result); // ALICE, BOB, CHARLIE
    }
}

Terminal Operations: minBy(), maxBy(), groupingBy()

The minBy() collector finds the minimum element in a stream based on a specified Comparator and returns the result as an Optional.

SHOW CODE
Optional min = Stream.of(3, 5, 1, 2)
    .collect(Collectors.minBy(Comparator.naturalOrder()));
System.out.println(min.orElse(-1)); // Output: 1

The maxBy() collector finds the maximum element in a stream based on a specified Comparator and returns the result as an Optional.

SHOW CODE
Optional max = Stream.of(3, 5, 1, 2)
    .collect(Collectors.maxBy(Comparator.naturalOrder()));
System.out.println(max.orElse(-1)); // Output: 5

The groupingBy() collector groups the elements of a stream based on a classification function and returns the grouped data as a Map.

SHOW CODE
Map<Integer, List<String>> groupedByLength = Stream.of("cat", "dog", "bird", "fox")
    .collect(Collectors.groupingBy(String::length));
System.out.println(groupedByLength);
// Output: {3=[cat, dog, fox], 4=[bird]}


Numeric Stream

A Numeric Stream in Java is a specialized type of stream that operates specifically on numeric values. There are three types of numeric streams: IntStream, LongStream, and Double Stream, which handle int, long, and double values, respectively. Numeric streams eliminate the overhead of autoboxing and provide commonly used methods such as sum() and max() for performing operations on numeric data, making them more efficient compared to regular Stream types.

SHOW CODE
IntStream.range(1, 5).forEach(System.out::println);
// Output: 1 2 3 4

IntStream.rangeClosed(1, 5).forEach(System.out::println);
// Output: 1 2 3 4 5

long count = IntStream.range(1, 10).count();
System.out.println(count);  // Output: 9

int sum = IntStream.range(1, 5).sum();
System.out.println(sum);  // Output: 10 (1 + 2 + 3 + 4)

OptionalInt max = IntStream.range(1, 5).max();
max.ifPresent(System.out::println);  // Output: 4

OptionalInt min = IntStream.range(1, 5).min();
min.ifPresent(System.out::println);  // Output: 1

OptionalDouble average = IntStream.range(1, 5).average();
average.ifPresent(System.out::println);  // Output: 2.5

List<String> numbers = Arrays.asList("1", "2", "3", "4");
IntStream intStream = numbers.stream().mapToInt(Integer::parseInt);
intStream.forEach(System.out::println);  // Output: 1, 2, 3, 4

List<String> numbers = Arrays.asList("1.1", "2.2", "3.3", "4.4");
DoubleStream doubleStream = numbers.stream().mapToDouble(Double::parseDouble);
doubleStream.forEach(System.out::println);  // Output: 1.1, 2.2, 3.3, 4.4

List<String> numbers = Arrays.asList("10", "20", "30", "40");
LongStream longStream = numbers.stream().mapToLong(Long::parseLong);
longStream.forEach(System.out::println);  // Output: 10, 20, 30, 40

IntStream intStream = IntStream.range(1, 5);
Stream<String> stringStream = intStream.mapToObj(Integer::toString);
stringStream.forEach(System.out::println);  // Output: "1", "2", "3", "4"