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. Anenum
provides a type-safe way to handle constant values, and it can be used in aswitch
statement or for comparison using==
. Compared to afinal
constant in Java, anenum
can contain fields, methods, and constructors. Thevalues()
method is used to retrive all the constants in an enum, and thevalueOf()
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
, andV
, 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 typeT
must be a subclass ofSomeClassOrInterface
orSomeClassOrInterface
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 typeT
must be a super class ofSomeClassOrInterface
orSomeClassOrInterface
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 typeT
must extend the classclassType
and implement bothInterface1
andInterface2
.
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 thejava.lang.Class
class. Some of the key classes and methods in this API include:
Class
: Represents the class of an object and provides methods likegetName()
,getDeclaredMethods()
,getDeclaredFields()
, and others to examine the class’s metadata.Method
: Represents a method of a class and allows invoking it dynamically using methods likeinvoke()
.Field
: Represents a field (variable) in a class and allows accessing or modifying its value dynamically.Constructor
: Represents a constructor in a class and allows creating new instances dynamically using methods likenewInstance()
.
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. TheObjectOutputStream
class is used to write the serialized object to a file or another output stream. AserialVersionUID
is automatically generated if not explicitly defined, but it is recommended to provide one manually to ensure compatibility during deserialization. Fields marked astranscient
are not serialized. Additionally, any objects regerenced by the serialized object are also serialized, provided they implement theSerializable
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
- 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.
- Collections allow data modification, whereas Streams provide read-only access to elements.
- Operations on Collections are executed eagerly, while Streams operations are lazy and only executed when a terminal operation is invoked.
- 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 theflatMap
method transforms and flaten streams into a single stream.
distinct(), count(), sorted()
The
distinct()
method eliminates duplicate elements from the stream. Thecount()
method calculates the total number of elements in the stream. Thesorted()
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. Theskip()
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. TheanyMatch()
method determines if any element in the stream matches the specified predicate. ThenoneMatch()
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. ThefindFirst()
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 aSupplier
.
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()
orStream.iterate()
, always use operations likelimit()
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 singleString
. 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 along
.
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 anOptional
.
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 anOptional
.
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 aMap
.
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
, andDouble Stream
, which handleint
,long
, anddouble
values, respectively. Numeric streams eliminate the overhead of autoboxing and provide commonly used methods such assum()
andmax()
for performing operations on numeric data, making them more efficient compared to regularStream
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"