JavaSE复习
参考视频:【狂神说Java】JavaSE阶段回顾总结
简单的就不讲了,比如什么break和continue区别,甚至一些什么什么继承封装多态的概念等等。 主要写一些Java特有的,重点的基础知识。主要还是例子和代码更多,更有利于理解。
java SE、 java ME、 java EE
- Java SE (Java Standard Edition):标准版
- Java EE (Java Enterprise Edition):企业版
- Java ME (Java Micro Edition):微型版
JDK?JRE?
JDK (Java Development Kit)软件开发工具包
JRE(Java Runtime Environment)运行环境
如果用户只需要运行,不需要开发,那只安装JRE即可。
复习helloworld
要求做到打开记事本能手写,并用javac编译java运行。
package com.zy;public class hello {public static void main(String[] args) {System.out.println("hello world!");}
}
数据类型
注意C++中char是1个字节的,java中char是2个字节(java内部都是用unicode的,所以java中的char是2个字节),C++中char应该对应byte
扩展,ASCII,Unicode,utf-8的区别
Unicode是字符集,UTF-8是编码规则
<< >> 左移,右移运算符
<<< >>> 无符号左移,右移运算符,高位补0
求-15>>2 和 -15>>>2:-4和1073741820
解释:一个是带符号右移两位,一个是无符号右移两位。
java包机制
域名倒写:规范写法: 公司域名倒着写+模块名字 com.nuc.maneger
例子:比如 com.baidu.video,因为java内部实际上是以文件夹形式存在的,是按com,baidu,video依次生成文件夹的
具体功能的是子文件夹,所以要倒着写。
package:无论什么时候都在第一行
为了更好地组织类,Java提供了包机制,用于区别类名的命名空间。
-
包名与目录结构:
package
声明的包名需要与文件所在的目录结构严格对应。例如,假设有以下包声明:package com.example.myproject;
该文件应该位于以下路径中:
/path/to/project/com/example/myproject/YourClass.java
-
默认包:如果你没有指定
package
,类会被放入一个默认包中。这时文件可以直接放在项目的根目录下。但在实际开发中,不推荐将类放在默认包中,因为这样会造成组织混乱和潜在的类名冲突。
一个文件只能有一个public类,且必须与文件名一直(大小写也要匹配)
javaDoc:通过注释生成程序帮助文档。
JDK帮助文档就是通过javaDoc生成的。
常见格式
/*** Method description.* @return description of return value*/
public int methodName() {}
可选注释参数:
@author:标签用于指定作者信息
@param:用于标记方法的参数说明。
@return:用于标记方法的返回值说明。
@throws 或 @exception:用于标记方法可能抛出的异常说明。
@see:用于指定参考文档的链接。
@since:用于指定API的版本信息。
@deprecated:用于标记已过时的API。
@inheritDoc:用于继承父类或接口的文档注释。
@link:类似于@see,用于创建链接到其他文档的标签。
javadoc
encoding指定用于读取源文件的字符编码。
charset 指定生成的 HTML 文档使用的字符集编码
Scanner对象输入文本
package com.zy;import java.util.Scanner;public class hello {public static void main(String[] args) {Scanner scanner = new Scanner(System.in); // 创建Scanner对象System.out.println("输入文本:");String userInput = scanner.nextLine(); // 读取用户输入的一行文本System.out.println("你输入了:" + userInput);scanner.close();}
}
foreach的使用
package com.zy;public class hello{public static void main(String[] args) {int[] numbers = {1,2,3,4,5};for(int num : numbers) {System.out.println(num);}}
}
java中带标签的break和continue
package com.zy;public class hello{public static void main(String[] args) {outLoop:for(int i=0; i<10;i++) { // 带个标签outLoopfor(int j=0;j<5;j++) {System.out.printf("%d,%d\n",i,j);break outLoop; // 直接跳出最外层循环。}}}
}
java可变长参数
package com.zy;class People {int a;
}public class hello{public static void test(Integer number,String...name) {System.out.println(number.getClass());System.out.println(name.getClass());System.out.println(number);for(String s : name) {System.out.println(s);}}public static void main(String[] args) {test(3, "lilin","hali");}
}
输出
class java.lang.Integer
class [Ljava.lang.String; //[表示一维数组,L表示是String类型。(如果是二位数组则为[[)
3
lilin
hali
Arrays工具类
在java中,数组本身没有方法可以调用,于是在API中提供了工具类Arrays供我们使用。
binarySearch():在已排序的数组中查找指定元素的索引。
copyOf():将一个数组的部分或全部元素复制到一个新数组中。
copyOfRange():将一个数组的指定范围内的元素复制到一个新数组中。
1.toString()
:将数组转换为一个字符串,便于打印和调试。
int[] ints = {1,2,3,4};System.out.println(ints); // 直接打印地址:[I@1540e19dSystem.out.println(Arrays.toString(ints)); // [1, 2, 3, 4]System.out.println(Arrays.toString(ints).charAt(0)); // [
2.fill()
:将数组的某个范围的所有元素都设置为指定值。
int[] arr = new int[5];Arrays.fill(arr,1,3,4); //左开右闭System.out.println(Arrays.toString(arr)); // [0, 4, 4, 0, 0]
3.sort(数组)
int[] arr = {4,2,6,3,8};System.out.println("升序前:"+Arrays.toString(arr)); // 升序前:[4, 2, 6, 3, 8]Arrays.sort(arr);System.out.println("升序后:"+Arrays.toString(arr)); // 升序后:[2, 3, 4, 6, 8]
4.sort(数组,排序规则)
在 Java 中,对于 基本类型(如 int[]
),Arrays.sort()
只能进行升序排序。如果要实现降序排序,你需要先将数组转换为包装类型,如 Integer[]
,然后使用自定义的比较器 (Comparator
) 进行降序排列。
int[] array = {1, 3, 2, 5, 4};// 1.将int[]转换为Integer[]Integer[] arrInteger = Arrays.stream(array).boxed().toArray(Integer[]::new);// 2.使用 Arrays.sort() 和 Collections.reverseOrder() 进行降序排序Arrays.sort(arrInteger,Collections.reverseOrder());// 3.打印排序后的结果System.out.println(Arrays.toString(arrInteger));
或者是,先升序排序,然后将数组中元素反转。
int[] array = {1, 3, 2, 5, 4};Arrays.sort(array);for(int i=0;i<array.length/2;i++) {int tmp = array[i];array[i] = array[array.length-i-1];array[array.length-i-1] = tmp;}System.out.println(Arrays.toString(array));
如果我想对自定义的类型数组进行排序呢?(比如:Student类的数组),这里给出两种方法:
1.实现Comparable接口,完成自定义排序
package com.zy;import java.util.Arrays;
import java.util.Collections;// 定义 Student 类,并实现 Comparable 接口
class Student implements Comparable<Student> {String name;int age;public Student(String name, int age) {this.name = name;this.age = age;}@Overridepublic int compareTo(Student other) {return this.age - other.age;}@Overridepublic String toString() {return name + ":" + age;}
}public class hello {public static void main(String[] args) {Student[] students = {new Student("Alice",22),new Student("Bob",18),new Student("Charlie",20),};Arrays.sort(students);System.out.println(Arrays.toString(students));}
}
上面方式看起来不太简洁,需要修改类本身。更灵活的方式如下:
2.利用匿名Comparator
对象,灵活地定义多种不同的排序规则。
package com.zy;import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;class Student{String name;int age;public Student(String name, int age) {this.name = name;this.age = age;}@Overridepublic String toString() {return name + ":" + age;}
}public class hello {public static void main(String[] args) {Student[] students = {new Student("Alice",22),new Student("Bob",18),new Student("Charlie",20),};// 按年龄排序Arrays.sort(students, new Comparator<Student>() {@Overridepublic int compare(Student o1, Student o2) {return o1.age - o2.age;}});System.out.println(Arrays.toString(students));// 按姓名排序Arrays.sort(students, new Comparator<Student>() {@Overridepublic int compare(Student o1, Student o2) {return o1.name.compareTo(o2.name);}});System.out.println(Arrays.toString(students));}
}
5.copyOfRange(original,from,to)
original:第一个参数为要拷贝的数组对象
from:第二个参数为拷贝的开始位置(包含)
to:第三个参数为拷贝的结束位置(不包含)
继承,封装,多态
写一遍经典的Animal继承,体会一下Java面向对象三大特性。
package com.zy;class Animal {private String name;private int age;public Animal(String name,int age) {this.name = name;this.age = age;}// 封装:getter 和 setter 方法public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}public void describe() {System.out.println("This is an animal named" + name + ", age" + age);}
}// 子类Dog继承父类Animal
class Dog extends Animal {private String breed;// 子类构造函数,调用父类的构造函数public Dog(String name, int age, String breed) {super(name, age); // 继承:调用父类构造函数this.breed = breed;}public String getBreed() {return breed;}public void setBreed(String breed) {this.breed = breed;}@Overridepublic void describe() {super.describe(); // 调用父类describe方法System.out.println("This is a " + breed + "dog.");}// Dog 类特有方法public void bark() {System.out.println("woof woof!");}
}public class Hello {public static void main(String[] args) {Animal animal = new Animal("Generic Animal", 5);animal.describe(); // 调用 Animal 类的方法Dog dog = new Dog("Buddy",3,"Golden Retriever");dog.describe(); // 调用重写后的describe方法dog.bark(); // 调用Dog类的bark方法// 多态体现Animal animalDog = new Dog("Max",2,"Beagle");animalDog.describe(); // 调用的是子类Dog的describe方法
// animalDog.bark(); // 错误,Animal没有bark()// 检查对象类型,使用instance ofif(animalDog instanceof Dog) {Dog specificDog = (Dog)animalDog; //向下转型specificDog.bark();}}
}
output:
This is an animal named Generic Animal, age 5
This is an animal named Buddy, age 3
This is a Golden Retriever dog.
Woof Woof!
This is an animal named Max, age 2
This is a Beagle dog.
Woof Woof!
The dog's new name is: Charlie
等等,既然animalDog instanceof Dog
为真,也就是animalDog 就是Dog类型,那为什么还需要向下转型(Dog)animalDog
??
如果有这种疑问,说明有两个问题:1. 对instanceof不熟悉;2.对向上转型和向下转型不熟悉。
instanceof
instanceof
是 Java 中的一个关键字,用于判断一个对象是否是某个类或其子类的实例。
举个例子
现在有类的继承链如下:
Object > Person > Teacher
Object > Person > Student
Object > String
Object obj = new Student();
obj instanceof Student; // true
obj instanceof Person; // true
obj instanceof Object; // true
obj instanceof String; // false
obj instanceof Teacher; // falseObject obj = new Person();
obj instanceof Student; // false
obj instanceof Teacher; // falsePerson person = new Student();
person instanceof Student; // true
person instanceof Person; // true;
person instanceof Object; // true;
person instanceof String; // 编译错误
person instanceof Teacher; // false// null用instanceof跟任何类型比较时都是false
null instanceof Object; // false
所以
if(animalDog instanceof Dog)
是判断animalDog 是否为Dog类型或其子类。
关于java中向上转型和向下转型
向上转型(Upcasting):子类对象转换为父类类型,把子当父用。向上转型通常是隐式进行的,意味着你不需要手动指定转换操作。
Dog dog = new Dog();Animal animal = dog; // 向上转型,隐式进行animal.makeSound(); // 输出 "Bark",即调用子类的重写方法
向上转型是安全的,不会发生
ClassCastException
。访问限制:向上转型后,编译器只能识别父类的成员(属性和方法),不能访问子类特有的方法或属性。
向下转型(Downcasting):向下转型是指将父类类型的引用转换为子类类型。这种转换是显式进行的,需要手动指定转换操作,并且存在一定的风险,向下转型时,必须确保父类对象实际上是该子类的实例,否则会抛出 ClassCastException
。
Animal animalDog = new Dog("Max",2,"Beagle");
if(animalDog instanceof Dog) {Dog specificDog = (Dog)animalDog; //向下转型specificDog.bark();}
向下转型之前,通常使用
instanceof
关键字检查对象是否属于某个子类,以防止ClassCastException
异常。
接口interface
注意,java是单继承,只能继承一个父类,但是可以实现多个接口。
记得本科时候Java老师说过,类描述一个实体,接口描述一组动作。
接口是 Java 中的一种引用类型,它类似于一个契约或协议,只能定义方法的签名(方法名、返回类型和参数),不能包含方法的具体实现。
class Animal {protected String name;Animal(String name) {this.name = name;}
}interface IFlying {void fly();
}interface IRuning {void run();
}interface ISwimming {void swim();
}// 猫会跑
class Cat extends Animal implements IRuning{Cat(String name) {super(name);}@Overridepublic void run() {System.out.println(this.name + "is running");}
}// 鱼会游泳
class Fish extends Animal implements ISwimming {Fish(String name) {super(name);}@Overridepublic void swim() {System.out.println(this.name + "is swimming");}
}// 狗既能跑,又能游泳
class Dog extends Animal implements IRuning, ISwimming {Dog(String name) {super(name);}@Overridepublic void run() {System.out.println(this.name + "is running");}@Overridepublic void swim() {System.out.println(this.name + "is swimming");}
}// 鸭子 水陆空三栖
class Duck extends Animal implements IRuning,ISwimming,IFlying {Duck(String name) {super(name);}@Overridepublic void run() {System.out.println(this.name + "is running");}@Overridepublic void swim() {System.out.println(this.name + "is swimming");}@Overridepublic void fly() {System.out.println(this.name + "is flying");}
}
default
方法
- Java 8 引入了
default
方法,允许在接口中提供默认方法实现。这是为了在不破坏现有实现的情况下扩展接口。default
方法必须使用public
访问级别,因为接口中的所有方法默认都是public
的(即使不显式声明)
public interface MyInterface {default void doSomething() {System.out.println("Default implementation");}
}
java接口实际上是可以定义变量的,但是他们有一下特性:
隐式 public static final
修饰:接口中的变量默认是 public
、static
和 final
public
:意味着接口中的变量是公开的,可以在任何地方访问。
static
:意味着变量属于接口,而不是某个具体的实现类。可以通过接口名直接访问。
final
:意味着变量是常量,一旦赋值就不能修改。
所以,接口中的变量实际上是常量,用于共享和定义全局的常量值。
必须初始化:由于接口中的变量是 final
,所以必须在定义时进行初始化。
interface MyInterface {int NUM = 1000;void doSomething();
}public class Hello implements MyInterface{@Overridepublic void doSomething() {System.out.println("NUM:" + NUM);}public static void main(String[] args) {System.out.println(NUM);System.out.println(MyInterface.NUM);}
}
函数式接口(jdk8引入)
- 只有一个抽象方法的接口(可以有很多默认方法)被称为函数式接口。函数式接口可以使用 lambda 表达式来简化代码。在 Java 中,
@FunctionalInterface
可检测接口是否符合函数式接口。
java Lambda表达式:在 Java 8 中引入了 Lambda 表达式,主要用于简化对函数式接口的实现。ambda 表达式允许你以更简洁的方式写出匿名内部类,尤其是对于需要传递行为(如排序、遍历、事件处理等)的场景。Lambda表达式语法:
(parameters) -> expression
或者
(parameters) -> { statements; }
关于Lambda的介绍可参考,要理解Lambda这个表达式实际上是一个接口类型。后面就不难理解了。
使用 Lambda 表达式简化函数式接口的例子:使用 Comparator
排序。
传统方法:
List<String> names = Arrays.asList("John", "Jane", "Doe");
Collections.sort(names, new Comparator<String>() {@Overridepublic int compare(String s1, String s2) {return s1.compareTo(s2);}
});
Lambda简化:
List<String> names = Arrays.asList("John", "Jane", "Doe");
Collections.sort(names, (s1, s2) -> s1.compareTo(s2));
java8中定义了很多函数式接口,供我们使用。用法可参考:https://blog.csdn.net/weixin_58473601/article/details/125231163。
接口与抽象类的区别
接口更抽象。接口中的所有方法默认都是 abstract
的,而抽象类可以包含具体方法,因此接口的抽象程度比抽象类更高。
内部类
局部内部类
- 局部内部类是在方法或作用域内定义的类。它的作用范围仅限于定义它的代码块中,离开了这个范围,它就不可访问。
静态内部类
- 静态内部类是用
static
修饰的内部类。它与外部类的实例无关,可以独立创建实例。静态内部类更像是外部类的一个静态成员,可以直接通过外部类访问。
匿名内部类(重点)
- 匿名内部类是一种没有名字的内部类,常用于简化代码,尤其是在实现接口或继承类时。匿名内部类适合只需要使用一次的类。
- 它通常用于实现接口的方法或者继承类时,并在实例化的同时重写其中的方法。
interface Animal {void makeSound();
}public class Main {public static void main(String[] args) {Animal cat = new Animal() {@Overridepublic void makeSound() {System.out.println("Meow");}};// 当然,也可以用lambda简化:Animal cat = () -> System.out.println("miao");cat.makeSound(); // 输出:Meow}
}
Java在类中的各种权限:public protected,private, default
访问修饰符 | 类内部 | 同一包内 | 不同包中的子类 | 不同包中的类 |
---|---|---|---|---|
public | 是 | 是 | 是 | 是 |
protected | 是 | 是 | 是 | 否 |
default | 是 | 是 | 否 | 否 |
private | 是 | 否 | 否 | 否 |
注意:
- 类的修饰符:Java 的类可以使用
public
或default
(无修饰符)。类不能使用protected
或private
修饰符。 - 接口的修饰符:接口可以是
public
或default
,而接口的所有字段默认是public static final
,所有方法默认是public abstract
(Java 8 及以上允许接口中有default
和static
方法)。
异常
Throwable类(Error和Exception)
参考
Java的Throwable
类是Java语言中所有错误和异常的超类。它位于java.lang
包下,是Java异常处理机制的核心。Throwable
类及其子类实例代表了程序执行中可能出现的异常情况和错误。在Java中,Throwable
有两个主要的子类:Error
和Exception
。
- Error: 表示一些严重的错误,通常是不应该被应用程序尝试捕获的问题,如OutOfMemoryError,StackOverflowError等。
- Exception: 表示需要被应用程序捕获的异常条件,它又分为两类:
检查型异常(Checked Exceptions): 这种异常在编译时强制要求处理,比如IOException、SQLException等。(比较严重)
非检查型异常(Unchecked Exceptions): 这类异常包括运行时异常(RuntimeException)如NullPointerException、IndexOutOfBoundsException等,以及错误(Error)。
也就是说 检查型异常比较严重,需要处理!非检查型异常不严重,可以不处理。
五个捕获错误关键字
try
块:包含可能会抛出异常的代码。catch
块:一旦异常发生,控制权跳到catch
块,执行其中的异常处理代码。finally
块:无论是否发生异常,finally
块中的代码始终会执行。throw
:手动抛出一个异常。throws
:在方法签名中声明该方法可能抛出的异常。
public class Hello{// 使用throws字段,方法抛出ArithmeticException错误public static int divide(int a, int b) throws ArithmeticException {if(b == 0) {throw new ArithmeticException("Cannot divide by zero!");}return a / b;}public static void main(String[] args) {try {// 尝试进行除法操作int result = divide(10, 0);System.out.println("Result:" +result);} catch (ArithmeticException e) {System.out.println("Exception caught:" + e.getMessage());} finally {System.out.println("This is the finally block. Always executed!");}}
}
try-with-resources
try-with-resources 是 Java 7 引入的一种语法,用于简化资源管理,特别是处理那些实现了 AutoCloseable
接口的资源,如输入/输出流、数据库连接等。它的主要优点是可以确保在使用完资源后自动关闭,减少了资源泄露的风险。
- 自动关闭资源:在
try
代码块结束时,所有在括号内声明的资源都会自动调用其close()
方法,从而释放资源。 - 简化代码:通过自动管理资源的关闭,减少了手动编写
finally
代码块的需要。 - 异常处理:如果在
try
块中发生异常,资源依然会被关闭。同时,如果在关闭资源时也发生异常,这些异常会被捕获并可供后续处理。
语法
try (ResourceType resource1 = new ResourceType();ResourceType resource2 = new ResourceType()) {// 使用资源进行操作
} catch (ExceptionType e) {// 处理异常
}
// 资源在此处自动关闭
自定义异常
自定义异常可以继承 Exception
(受检异常)或 RuntimeException
(非受检异常)
1.自定义异常类,继承自 Exception
class InvalidAgeException extends Exception {public InvalidAgeException(String message) {super(message);}
}public class Hello{// 定义一个方法,抛出自定义异常public static void validateAge(int age) throws InvalidAgeException { // 注意!!这里throws不能省略if (age < 18) {// 当年龄小于 18 时,抛出自定义异常throw new InvalidAgeException("Age must be greater than or equal to 18");} else {System.out.println("Valid age: " + age);}}public static void main(String[] args) {try {// 调用 validateAge 方法,传入一个无效的年龄validateAge(15);} catch (InvalidAgeException e) {// 捕获并处理自定义异常System.out.println("Exception caught: " + e.getMessage());}}
}
2.使用 RuntimeException
创建非受检异常
class InvalidAgeException extends RuntimeException {public InvalidAgeException(String message) {super(message);}
}
非受检异常不需要在方法签名中声明 throws
,也不要求在编译时处理。也就是说,上面的validateAge
可以不用throws InvalidAgeException
。
一些常用类
Object类
在Java中,Object
类是所有类的根类。
下面是Object类中定义的一些方法
== 和 equals方法
equals 是 Object 中的方法.
== 是一个运算符,可以判断基本类型也可以判断引用类型。判断基本类型时,就是判断两者的值是否相等;判断引用类型时,就是判断两者的地址是否相等,也就是说判断实际对象是不是同一个。
equals 是 Object 中的方法,只能判断引用类型,也是通过判断两者地址是否相等。一般情况下,子类会重写这个 equals 方法,以添加判断内容是否相等的功能。
String str1 = "Hello";String str2 = "Hello";System.out.println(str1 == str2); // trueSystem.out.println(str1.equals(str2)); // trueString str3 = new String("Hello");String str4 = new String("Hello");System.out.println(str3 == str4); // falseSystem.out.println(str3.equals(str4)); // true
==
操作符用于比较两个引用是否指向同一个对象,而不是比较它们的值是否相同。 所以在第一个判断中,str1
和 str2
都是直接用字面量 "Hello"
初始化的。Java 编译器在加载字面量字符串时会将它们放入**字符串常量池(string pool)**中。如果两个字符串字面量相同,它们会引用池中的同一个对象。
字符串常量池:在使用字面量方式创建字符串时(如
String str = "Hello";
),Java 会在字符串常量池中检查是否已经存在相同内容的字符串。如果存在,它会复用池中的对象。
而第二个例子中使用 new
关键字创建字符串时,每次都会在堆中分配一个新的对象,即使两个对象的内容相同。所以str3 == str4
是 false
hashCode 方法
hashCode()
的主要目的是通过返回一个整数来表示对象的散列值(哈希码),这个值用于确定对象在基于哈希的数据结构中的位置。通过 hashCode()
,可以提高这些数据结构的性能,使其能够快速插入、查找和删除数据。
Person p1 = new Person();
Person p2 = new Person();
Person p3 = p2;
System.out.println(p1.hashCode()); // 356573597
System.out.println(p2.hashCode()); // 1735600054
System.out.println(p3.hashCode()); // 1735600054
在 Java 中,hashCode()
是 Object
类中的一个方法,所有 Java 类都继承自 Object
,因此每个 Java 对象都有一个 hashCode
方法。它的作用是返回对象的散列码(哈希码),它是用于支持基于哈希的数据结构(如 HashMap
、HashSet
、Hashtable
等)的内部算法的。
hashCode()
的作用
hashCode()
的主要目的是通过返回一个整数来表示对象的散列值(哈希码),这个值用于确定对象在基于哈希的数据结构中的位置。通过 hashCode()
,可以提高这些数据结构的性能,使其能够快速插入、查找和删除数据。
hashCode()
和 equals()
的关系
在使用基于哈希的数据结构时,hashCode()
和 equals()
方法通常是一起工作的。它们之间有一个重要的约定:
- 如果两个对象根据
equals()
方法是相等的,那么它们的hashCode()
必须相同。 - 如果两个对象的
hashCode()
不相同,那么它们肯定不是相等的。 - 如果两个对象的
hashCode()
相同,它们未必是相等的,但会被认为是在同一个哈希桶中,需要进一步通过equals()
来确定它们是否真正相等。
来看看String类型的hashCode运行结果。
String str1 = "Hello";String str2 = "Hello";String str3 = new String("Hello");String str4 = new String("Hello");System.out.println(str1.hashCode()+" "+str2.hashCode()); // 69609650 69609650System.out.println(str3.hashCode()+" "+str4.hashCode()); // 69609650 69609650
在 Java 中,hashCode()
方法的默认实现会根据对象的内存地址来生成哈希值。然而,对于像 String
这样的类,hashCode()
方法已经被重写,基于字符串的内容(字符序列)来生成哈希值,而不是对象的内存地址。这是为什么你会看到这些字符串对象的哈希码是相等的,即使它们是不同的对象。
toString 方法
在Object
类中,toString
的默认实现返回的是对象的全类名(包名加类名)和对象的哈希码。
System.out.println(new Person()); // com.zy.Person@1540e19d
当然,我们也可以重写toString方法。
class A{@Overridepublic String toString() {return "hahaha";}
}public class Hello{public static void main(String[] args) {System.out.println(new A()); // hahaha}
}
clone()方法
clone()
方法默认是浅拷贝,如果对象包含引用类型的数据,只有对象的引用会被复制,实际对象不会被复制。
展示深浅拷贝
getClass()方法
返回运行时对象的类类型。它返回一个 Class
对象,用于表示此对象的类。可以用来获取对象的类型,通常与反射一起使用。
class Person{
}public class Hello{public static void main(String[] args) {Person person = new Person();System.out.println(person.getClass()); // class com.zy.Person}
}
notify()方法
唤醒等待在此对象上的一个线程,必须在同步块中使用。
wait()方法
使当前线程等待,释放锁,直到被唤醒或超时,也必须在同步块中使用。
Math类
Math类很多,大概看一下吧。需要用到再查即可。
abs():返回一个数的绝对值
max():返回两个数中较大的那个数
min():返回两个数中较小的那个数
pow():返回一个数的某个次幂
sqrt():返回一个数的平方根
sin():返回一个数的正弦值
cos():返回一个数的余弦值
tan():返回一个数的正切值
log():返回一个数的自然对数
exp():返回一个数的指数值
ceil():返回一个数的上限整数
floor():返回一个数的下限整数
round():返回一个数的四舍五入整数
Random类
在 Java 中,Random
类是用于生成伪随机数的类。它位于 java.util
包中,提供了一系列的方法来生成不同类型的随机数。Random
类不是线程安全的,因此在多线程环境中可能需要使用 ThreadLocalRandom
或者 SecureRandom
类。
构造方法
Random random = new Random(); // 使用当前时间作为种子
Random random2 = new Random(42L); // 使用指定的种子
常用方法
Random random = new Random();System.out.println(random.nextInt()); // 随机生成一个整数,这个整数的范围就是int类型的范围-2^31~2^31-1System.out.println(random.nextInt(100)); // 生成[0,100)之间的一个整数System.out.println(random.nextLong()); // 随机生成long类型范围的整数System.out.println(random.nextFloat()); // 随机生成[0, 1.0)区间的小数System.out.println(random.nextDouble()); // 随机生成[0, 1.0)区间的小数System.out.println(random.nextBoolean()); // 随机生成一个boolean值,生成true和false的值几率相等,也就是都是50%的几率System.out.println(random.nextGaussian()); // //随机生成呈高斯(“正态”)分布的 double 值,其平均值是 0.0,标准差是 1.0byte[] byteArr = new byte[5];random.nextBytes(byteArr); // 随机生成byte,并存放在定义的数组中,生成的个数等于定义的数组的个数System.out.println(Arrays.toString(byteArr));
File类
实现创建,写入,读取,删除文件。
try {// 1.创建文件File file = new File("example.txt");if(file.createNewFile()) {System.out.println();} else {System.out.println("File already exitsts");}// 2.写入文件FileWriter writer = new FileWriter(file);writer.write("This is a sample text");writer.close();System.out.println("Successfully wrote to the file.");// 3.读取文件FileReader reader = new FileReader(file);// 用BufferedReader更高效的读取数据。并且它还提供了readLine()方法。BufferedReader bufferedReader = new BufferedReader(reader);String line;System.out.println("Reading the file:");while ((line = bufferedReader.readLine()) != null) {System.out.println(line);}bufferedReader.close();// 4.删除文件if (file.delete()) {System.out.println("File deleted: " + file.getName());} else {System.out.println("Failed to delete the file.");}}catch (IOException e) {System.out.println("An error occurred.");e.printStackTrace();}
包装类
基本数据类型 | 包装类 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
装箱(Boxing)
装箱是将基本数据类型转换为其对应的包装类对象的过程。装箱分为手动装箱和自动装箱(从 Java 5 开始支持)。
手动装箱
int a = 10;
Integer boxedA = new Integer(a); // 手动装箱
自动装箱
int b = 20;
Integer boxedB = b; // 自动装箱
拆箱(Unboxing)
手动拆箱
Integer c = new Integer(30);
int unboxedC = c.intValue(); // 手动拆箱
自动拆箱
Integer d = 40;
int unboxedD = d; // 自动拆箱
装箱与拆箱的应用场景
在使用集合类(如 ArrayList
, HashMap
等)时,集合类只能存储对象,不能存储基本数据类型。这时,基本数据类型需要包装成包装类对象。这就是自动装箱的一个常见场景。
ArrayList<Integer> list = new ArrayList<>();
list.add(100); // 自动装箱,将 100 转换为 Integer 对象
int num = list.get(0); // 自动拆箱,将 Integer 对象转换为 int
包装类的缓存机制
对于某些数值,包装类会缓存对象。例如,Integer
类会缓存 -128
到 127
之间的数值对象,因此这些范围内的装箱值可以被复用,不会每次都创建新的对象。(好像想起来了,当时Java老师还给我们举了类似下面这个例子)
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true,因为 127 在缓存范围内Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false,因为 128 不在缓存范围内// 注意:== 和 equals() 的区别:对于包装类对象,== 比较的是对象的引用,而 equals() 比较的是对象的值。
Data类
Date
类是 Java 中表示日期和时间的类,它提供了一些基本的日期和时间操作方法。这个类的实例包含一个时间戳,表示自 Unix 纪元(1970年1月1日)以来的毫秒数。
常用方法
new Date()
:获取当前日期和时间。getTime()
:返回自 1970 年 1 月 1 日 UTC 以来的毫秒数。
import java.util.Date;public class DateExample {public static void main(String[] args) {Date now = new Date(); // 获取当前日期时间System.out.println("当前时间: " + now);long timeInMillis = now.getTime(); // 获取当前时间的时间戳(毫秒数)System.out.println("时间戳: " + timeInMillis);}
}// 当前时间:Thu Sep 26 14:39:35 CST 2024
// 时间戳:1727332775366
SimpleDateFormat类
SimpleDateFormat
是一个用于格式化和解析日期的类。它可以将日期格式化为指定的字符串,或将字符串解析为日期对象。
常用方法:
format(Date)
:将日期格式化为字符串。parse(String)
:将字符串解析为日期对象。
import java.text.SimpleDateFormat;
import java.util.Date;public class SimpleDateFormatExample {public static void main(String[] args) {Date now = new Date(); // 获取当前时间SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 定义日期格式String formattedDate = sdf.format(now); // 格式化日期System.out.println("格式化后的日期: " + formattedDate);// 解析字符串为日期try {Date parsedDate = sdf.parse("2023-09-21 12:00:00");System.out.println("解析后的日期: " + parsedDate);} catch (Exception e) {e.printStackTrace();}}
}// 格式化后的日期:2024-09-26 14:43:17
// 解析后的日期: Thu Sep 21 12:00:00 CST 2023
Calendar类
Calendar
类是一个更强大的日期类,它不仅可以获取日期和时间,还可以对日期进行操作(如加减年、月、日等)。虽然 Date
类是简单的日期表示,Calendar
提供了更多的操作方法和功能。Calendar
类是抽象类,通常使用 Calendar.getInstance()
来创建一个 GregorianCalendar
对象。
常用方法
getInstance()
:获取一个Calendar
实例,表示当前时间。get(int field)
:获取指定时间字段的值,如Calendar.YEAR
,Calendar.MONTH
等。add(int field, int amount)
:对指定时间字段进行加减操作。
import java.util.Calendar;public class CalendarExample {public static void main(String[] args) {Calendar calendar = Calendar.getInstance(); // 获取当前日期时间的 Calendar 实例int year = calendar.get(Calendar.YEAR); // 获取年份int month = calendar.get(Calendar.MONTH) + 1; // 获取月份,月份从 0 开始,因此要加 1int day = calendar.get(Calendar.DAY_OF_MONTH); // 获取日期System.out.println("当前日期: " + year + "-" + month + "-" + day);// 对日期进行操作calendar.add(Calendar.YEAR, 1); // 当前时间加 1 年int newYear = calendar.get(Calendar.YEAR);System.out.println("加一年后的年份: " + newYear);}
}
String 类
String
类用于表示不可变的字符串。每当对字符串进行修改时,实际上会创建一个新的 String
对象,而不是修改原有的字符串。
特点:
- 不可变性:
String
对象一旦创建,它的内容就不能被改变。每次修改(如拼接、替换)时,都会生成新的String
对象。 final
修饰符:String
类被final
修饰,无法被继承。- 效率低(操作多时):由于不可变性,每次修改字符串都会创建新的对象,所以当操作大量字符串时,效率较低。
适用场景:
- 字符串的内容不需要频繁改变。
- 操作量较少的字符串,如常量、配置信息等。
StringBuffer 类
StringBuffer
类用于表示可变的字符串。与 String
不同,StringBuffer
可以在不创建新对象的情况下修改其内容。
特点:
- 可变性:
StringBuffer
对象的内容可以直接进行修改,如追加、插入、删除等操作。 - 线程安全:
StringBuffer
是线程安全的,它的操作是同步的(内部使用synchronized
关键字进行方法锁定),因此在多线程环境下,多个线程可以安全地修改同一个StringBuffer
对象。 - 效率相对低:由于线程安全机制的存在,
StringBuffer
在多线程的环境下效率相对较高,但在单线程环境下由于同步操作,效率低于StringBuilder
。
适用场景:
- 需要在多线程环境下频繁修改字符串的场合。
- 需要保证线程安全的字符串操作。
public class StringBufferExample {public static void main(String[] args) {StringBuffer sb = new StringBuffer("Hello");sb.append(" World"); // 直接修改当前对象System.out.println(sb); // 输出 "Hello World"}
}
StringBuilder 类
StringBuilder
类也是一个可变的字符串类,与 StringBuffer
类似,能够修改字符串内容。不同的是,StringBuilder
不保证线程安全。
特点:
- 可变性:与
StringBuffer
一样,StringBuilder
对象的内容可以直接修改。 - 非线程安全:
StringBuilder
没有同步机制,因此在多线程环境下是不安全的,但在单线程环境下效率更高。 - 效率高:由于没有线程同步的开销,在单线程场景中,
StringBuilder
的性能要比StringBuffer
高。
适用场景:
- 在单线程环境下需要频繁修改字符串时,
StringBuilder
是最佳选择。 - 大量字符串拼接或修改的场合。
public class StringBuilderExample {public static void main(String[] args) {StringBuilder sb = new StringBuilder("Hello");sb.append(" World"); // 直接修改当前对象System.out.println(sb); // 输出 "Hello World"}
}
String三兄弟对比
特性 | String | StringBuffer | StringBuilder |
---|---|---|---|
可变性 | 不可变 | 可变 | 可变 |
线程安全性 | 线程安全(不可变) | 线程安全(使用同步机制) | 非线程安全 |
性能 | 操作大量时性能低 | 操作大量时性能一般(同步) | 操作大量时性能高(单线程) |
适用场景 | 操作少、不变字符串 | 多线程环境中字符串操作 | 单线程环境中字符串操作 |
// 视频中的一个小测试
// "a" + 1 + 2 = ?
// 'a' + 1 + 2 = ?
// 1 + 2 + "a" = ?public class hello{public static void main(String[] args) {String str1 = "a" + 1 + 2; // a12
// String str2 = 'a' + 1 + 2; // 报错,结果是int类型String str3 = 1 + 2 + "a"; // a3System.out.println(str1+" " +str3);}
}
集合框架(Collection Framework)
后续用Java刷题会接触的更多,所以现在就大概了解一下即可。
集合框架提供了一套用于操作对象集合的标准接口和类,主要包括 Collection
和 Map
两大核心接口。
Collection
专注于 元素的管理。(一个元素)Map
专注于 键值对的映射和查找。(两个元素)
Collection 接口
List (有序且可重复):List
接口表示元素按插入顺序存储,允许重复元素。常见实现类有:
- ArrayList:基于动态数组实现,适合频繁读取数据的场合。
- LinkedList:基于链表实现,适合频繁插入和删除操作的场合。
- (不常用)Vector:与
ArrayList
类似,也是基于动态数组实现的集合类,但它是线程安全的,效率比ArrayList
略低。 - Stack:继承自
Vector
,实现了栈的数据结构,遵循后进先出(LIFO)的规则。
Set (无序且不可重复):Set
接口用于存储不重复元素的集合,不保证元素的插入顺序。常见实现类有:
- HashSet:基于哈希表实现,插入和查找操作的时间复杂度为 O(1)。(实际上HashSet内部是HashMap实现的)
- TreeSet:基于红黑树实现,元素有序,支持按自然顺序或比较器顺序排序。(内部由TreeMap实现)
Map 接口
Map
接口用于存储键值对。每个键对应唯一的值,不允许重复的键。常见实现类有:
- HashMap(重点,面试常考):基于哈希表实现的
Map
,允许null
键和null
值。常用于快速查找、插入和删除。- 在 JDK 1.7 中,
HashMap
的底层结构是数组和链表的结合。(数组+链表) - 在 JDK 1.8 中,当链表长度超过一定阈值(默认为 8)时,
HashMap
使用红黑树替代链表,以提高性能。(数组+链表+红黑树)
- 在 JDK 1.7 中,
- TreeMap:基于红黑树实现的
Map
,保证键的有序性,适用于按自然顺序或自定义顺序排序的场景。
Iterator 接口
Iterator
是集合的迭代器接口,提供了遍历集合元素的标准方法。常用方法:
hasNext()
: 检查是否有下一个元素。next()
: 获取下一个元素。remove()
: 从集合中移除当前迭代器返回的元素。
Collections 工具类
Collections
是 Java 提供的一个工具类,位于 java.util
包中,包含了对集合进行操作的静态方法。它提供了对集合(如 List
, Set
, Map
等)进行排序、搜索、线程安全化等功能。
比如:排序:
sort(List<T> list)
:对List
进行自然顺序排序,T
必须实现Comparable
接口。sort(List<T> list, Comparator<? super T> c)
:使用Comparator
对List
进行排序。
其他:
reverse(List<?> list)
:反转List
中的元素顺序。shuffle(List<?> list)
:随机打乱List
中元素的顺序。
这个就不细讲了,用到再查即可。
泛型 <>
泛型 (Generic) 是 Java 在 JDK 5.0 引入的一项功能,旨在增强代码的类型安全性和可读性。通过使用泛型,可以在编译时进行类型检查,避免了强制类型转换带来的问题。
泛型的基本用法
泛型的好处:
- 类型安全:在编译时检查类型,减少了运行时的
ClassCastException
。 - 代码可读性增强:泛型使得代码更清晰易懂,减少了不必要的类型转换。
- 代码重用:泛型允许编写更具通用性的代码,适用于多种不同类型。
泛型的基本语法:
class Box<T> {private T value;public T getValue() {return value;}public void setValue(T value) {this.value = value;}
}
在上述例子中,T
是一个类型参数,可以在使用 Box
类时指定具体的类型。
Box<Integer> integerBox = new Box<>();
integerBox.setValue(10); // 自动装箱,避免了类型转换问题
int value = integerBox.getValue(); // 无需强制类型转换
可以看到,这里初始化的时候,右边是没有指定类型T的,因为编译器会自动推断出是Integer。这里实际上是用了 Java 7 引入的语法糖钻石运算符<>
。
Box<Integer> integerBox = new Box<>(); //java7前
Box<Integer> integerBox = new Box<Integer>(); // java7后
常见的泛型约束
? extends T
描述: ? extends T
表示该类型可以是 T
本身或者 T
的任何子类。
用途: 主要用于读取操作,即你只打算从数据结构中读取数据,不希望向其中写入。因为它可以保证读取出来的类型至少是 T
的类型或其子类。
List<? extends Number> list = new ArrayList<Integer>();
Number num = list.get(0); // 可以安全地读取
在这个例子中,list
可以是 List<Integer>
、List<Double>
等,只要是 Number
的子类即可。我们可以安全地读取 Number
或其子类的对象。
但是不能向 list
中添加元素,因为编译器无法确定具体类型:
list.add(10); // 编译错误
? super T
描述: ? super T
表示该类型可以是 T
本身或者 T
的任何父类。
用途: 主要用于写入操作,即你打算向数据结构中添加元素,因为它可以保证你添加的元素至少是类型 T
的对象。
List<? super Integer> list = new ArrayList<Number>();
list.add(10); // 可以安全地写入
在这个例子中,list
可以是 List<Number>
或 List<Object>
等,但不能确定其具体类型。
可以向 list
中添加 Integer
或 Integer
的子类对象,因为 list
至少接受 Integer
。但是读取操作有一定的限制,只能读取为 Object
,因为具体类型未知:
Object obj = list.get(0); // 只能保证读取为 Object
总结
? extends T
: 上界通配符,用于读取数据,保证类型为T
或其子类,但不适合写入。? super T
: 下界通配符,用于写入数据,保证可以安全地添加类型为T
或其子类的数据,但读取的数据只能被看作Object
。<?>
: 无界通配符,用于接受任意类型,多用于通用方法或类,但只能以Object
类型读取,不能写入。
泛型类与泛型方法
-
泛型类
:类定义时使用泛型,可以让类的属性、方法使用多种类型。
class Pair<T, U> {private T first;private U second;public Pair(T first, U second) {this.first = first;this.second = second;}public T getFirst() { return first; }public U getSecond() { return second; } }
-
泛型方法
:方法定义时使用泛型,允许该方法可以处理多种类型。
public <T> T getElement(List<T> list, int index) {return list.get(index); } // <T>:这是在方法名前声明的类型参数。表示该方法是一个泛型方法,并且 T 是一个类型参数。这个类型参数 T 可以是任何类型。 // T getElement(List<T> list, int index) T表示返回值
避免类型转换问题
在没有泛型之前,集合操作需要频繁进行强制类型转换,容易引发类型转换错误。例如:
List list = new ArrayList(); // 未使用泛型
list.add("Hello");
list.add(123);for (Object obj : list) {// 强制类型转换String str = (String) obj; // 运行时会抛出ClassCastException
}
通过泛型,类型可以在编译时得到检查,从而避免类型转换问题:
List<String> list = new ArrayList<>();
list.add("Hello");
// list.add(123); // 编译时会报错for (String str : list) {// 不需要强制类型转换System.out.println(str);
}
泛型消除了集合操作中手动进行类型转换的需要,确保了在编译时就能检测出类型错误,而不是等到运行时发生 ClassCastException
。
总结
Collections
工具类提供了大量处理集合的常用方法。- 泛型通过在编译时进行类型检查,避免了手动类型转换,增强了代码的安全性和可读性。
- 泛型约束(如
? extends T
,? super T
)为代码的灵活性提供了更多可能。
IO流
根据流的方向可以把IO流分为 输入流(读取)和输出流(写出)。
根据操作文件类型可以把IO流分为 字节流(所有类型文件)和字符流(纯文本文件)。
根据流的角色的不同分为:节点流,处理流。
字节流
字节流用于处理原始字节数据,比如图片、音频等非文本数据。Java 提供了 InputStream
和 OutputStream
作为字节流的基础类。(即字节流进而分类为输入流和输出流)
字节输出流OutputStream
OutputStream
是一个抽象类,提供了向输出目标(如文件、网络连接等)写入字节的基本方法。常见的方法包括:
void write(int b)
:写入一个字节(0-255),b
是字节的整数表示。void write(byte[] b)
:将字节数组中的所有字节写入输出流。void write(byte[] b, int off, int len)
:从字节数组的指定位置写入指定长度的字节到输出流。void flush()
:刷新输出流,确保所有缓存的数据都被写入目标。void close()
:关闭输出流,释放相关资源。
OutputStream
有很多子类,下面举个例子:使用FileOutputStream
写入文件。
public class Hello {public static void main(String[] args) throws IOException {String data = "Hello, OutputStream!";FileOutputStream fos = new FileOutputStream("output.txt");fos.write(data.getBytes());//fos.flush();fos.close();}
}
如果想追加写,可以使用构造的第二个参数boolean append
。
FileOutputStream fos = new FileOutputStream("output.txt",true);
字节输入流InputStream
nputStream
是一个抽象类,提供了从输入源(如文件、网络连接等)读取字节的基本方法。常见的方法包括:
int read()
:读取下一个字节,并返回该字节的整数值(0-255),返回 -1 表示到达流的末尾。int read(byte[] b)
:从输入流中读取字节并存储到字节数组中,返回实际读取的字节数。int read(byte[] b, int off, int len)
:从输入流中读取最多len
个字节,并将其存储到字节数组b
的指定偏移量off
中。void close()
:关闭输入流,释放相关资源。
示例:使用 FileInputStream
读取文件
public class Hello {public static void main(String[] args) throws IOException {try(FileInputStream fis = new FileInputStream("output.txt");) {int content;// 注意fis.read()返回的是int类型while((content = fis.read()) != -1) {System.out.print((char)content);}}catch (IOException e) {e.printStackTrace();}}
}
组合使用,复制一张图片
public static void main(String[] args) {try(FileInputStream fis = new FileInputStream("source.jpg");FileOutputStream fos = new FileOutputStream("des.jpg")) {byte[] buffer = new byte[1024]; //缓冲区int bytesRead;while((bytesRead = fis.read(buffer)) != -1) {fos.write(buffer, 0 ,bytesRead);}}catch (IOException e) {e.printStackTrace();}}
如果把上面代码缓冲区去掉,变成一个字节一个字节读写,会发现非常慢。
硬盘读写数据的最小单位是块,而不是字节。块的大小通常是 4KB 或更大。如果程序一次只处理一个字节,这与硬盘的最小块大小不匹配,可能会造成效率低下。通过缓冲区,程序一次可以读取多个字节,优化硬盘的读写操作。
大多数文件系统(如 NTFS、EXT4 等)的磁盘块大小通常是 4 KB,也就是 4096 字节。因此,1024 字节的缓冲区大小很容易与文件系统的块大小相结合。虽然 1 KB 小于 4 KB,但它仍然能提供足够的缓冲并减少频繁的读写调用。
字符流
字符流用于处理文本数据,比如文本文件。Java 提供了 Reader
和 Writer
作为字符流的基础类。
字符输入流Reader
Reader
是一个抽象类,用于读取字符流。常见的方法有:
int read()
:读取单个字符并返回对应的字符编码值(int
类型)int read(char[] cbuf)
:尝试读取多个字符并将其存储到字符数组cbuf
中int read(char[] cbuf, int off, int len)
boolean ready()
:返回是否可以无阻塞地读取字符,通常用于判断流是否准备好进行读操作。long skip(long n)
:跳过并丢弃流中最多n
个字符,返回实际跳过的字符数。void close()
String readLine()
(BufferedReader
独有):读取一行文本并返回一个字符串。如果到达流的末尾
常见的实现类有:
FileReader
:用于从文件中读取字符。BufferedReader
:为其他Reader
类提供缓冲能力,提高读取效率。
示例:使用FileReader读取文件。
public static void main(String[] args) {try(FileReader reader = new FileReader("output.txt")) {int character;while((character = reader.read()) != -1) {System.out.print((char) character);}} catch (IOException e) {e.printStackTrace();}}
使用BufferedReader
提高读取效率
public static void main(String[] args) {try(BufferedReader reader = new BufferedReader(new FileReader("output.txt"))) {String line;while((line = reader.readLine()) != null) {System.out.println(line);}} catch (IOException e) {e.printStackTrace();}}
字符输出流Writer
Writer
是一个抽象类,用于写入字符流。
void write(int c)
void write(char[] cbuf)
void write(char[] cbuf, int off, int len)
void write(String str)
void write(String str, int off, int len)
void flush()
void close()
常见的实现类有:
FileWriter
:用于将字符写入文件。BufferedWriter
:为其他Writer
类提供缓冲能力,提高写入效率。
示例:用FileWriter
写入
public static void main(String[] args) {try (FileWriter writer = new FileWriter("output.txt")) {writer.write("Hello, World!\n");writer.write("This is a test.");} catch (IOException e) {e.printStackTrace();}}
使用 BufferedWriter
提高写入效率:
public static void main(String[] args) {try (BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {writer.write("Hello, World!");writer.newLine(); // 添加换行符writer.write("This is a test.");} catch (IOException e) {e.printStackTrace();}}
节点流
节点流是直接与数据源或数据目的地交互的流。它们可以直接从文件、内存、管道等中读取或向这些位置写入数据。节点流是最基础的流,主要用于物理设备的输入/输出操作。
常见的节点流:
- FileInputStream 和 FileOutputStream:处理文件的字节输入和输出。
- FileReader 和 FileWriter:处理文件的字符输入和输出。
- CharArrayReader 和 CharArrayWriter:将字符数据存储到字符数组中。
- ByteArrayInputStream 和 ByteArrayOutputStream:处理内存中的字节数据。
- PipedInputStream 和 PipedOutputStream:实现线程间的管道通信。
处理流
处理流是一种包装在其他流上,提供额外功能的流。处理流不能直接与数据源或数据目的地交互,而是依赖于节点流来完成实际的输入输出操作。它们主要用于在节点流的基础上进行数据处理,比如缓冲、过滤、数据转换等。
常见的处理流:
- BufferedInputStream 和 BufferedOutputStream:通过缓冲区来提高字节输入/输出的效率。
- BufferedReader 和 BufferedWriter:通过缓冲区来提高字符输入/输出的效率。
- DataInputStream 和 DataOutputStream:允许从流中读写Java的基本数据类型(如int, long, double)。
- ObjectInputStream 和 ObjectOutputStream:用于序列化和反序列化Java对象。
- InputStreamReader 和 OutputStreamWriter:用于将字节流转换为字符流,或将字符流转换为字节流。
- PrintWriter 和 PrintStream:提供便捷的写入方法,比如
print()
和println()
,主要用于文本数据输出。
DataInputStream 和 DataOutputStream
示例
public static void main(String[] args) {// 写入数据try(DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.dat"))) {dos.writeInt(1234);dos.writeDouble(12.34);dos.writeBoolean(true);}catch (IOException e) {e.printStackTrace();}// 读取数据try(DataInputStream dis = new DataInputStream(new FileInputStream("data.dat"))) {int i = dis.readInt();double d = dis.readDouble();boolean b = dis.readBoolean();System.out.println("Read values: " + i + ", " + d + ", " + b);}catch (IOException e) {e.printStackTrace();}}
DataInputStream/DataOutputStream 提供了对基本数据类型进行读写的方便接口,避免了手动处理字节流。常用于需要将基本数据类型以二进制形式存储或传输的场景,比如保存配置文件、网络通信等。
ObjectInputStream 和 ObjectOutputStream
(序列化和反序列化)
ObjectInputStream:用于从输入流中反序列化对象,即将字节流转化为 Java 对象。
ObjectOutputStream:用于将 Java 对象序列化并写入输出流,即将对象转化为字节流。
示例
class Person implements Serializable {String name;int age;Person(String name, int age) {this.name = name;this.age = age;}public String toString() {return "Person{name='" + name + "', age=" + age + "}";}
}public class Hello {public static void main(String[] args) throws IOException, ClassNotFoundException {// 序列化对象到文件try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.dat"))) {Person person = new Person("Alice", 30);oos.writeObject(person);}// 从文件中反序列化对象try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.dat"))) {Person person = (Person) ois.readObject();System.out.println("Deserialized person: " + person);}}
}
PrintWriter 和 PrintStream
- PrintWriter:是字符流,用于格式化输出文本数据。它提供了很多便捷的
print
和println
方法,常用于写入文件或显示在控制台。 - PrintStream:是字节流,类似于
PrintWriter
,但它处理字节数据,常用于打印字节流到文件或控制台。
// 使用 PrintWriter 将字符写入文件try (PrintWriter pw = new PrintWriter(new FileWriter("printwriter.txt"))) {pw.println("Hello, PrintWriter!");pw.printf("Formatted string: %d, %.2f\n", 10, 3.14);}// 使用 PrintStream 将字节写入文件try (PrintStream ps = new PrintStream(new FileOutputStream("printstream.txt"))) {ps.println("Hello, PrintStream!");ps.printf("Formatted string: %d, %.2f\n", 10, 3.14);}
PrintWriter 适合处理字符数据,而 PrintStream 处理字节数据。System.out
本质上是一个 PrintStream
,可以直接用于输出到控制台。
序列化和反序列化
- 序列化:
ObjectOutputStream
,通过writeObject()
方法进行对象的序列化。 - 反序列化:
ObjectInputStream
,通过readObject()
方法进行对象的反序列化。
Serializable 接口
Serializable 是一个标记接口(没有任何方法),即空接口。当一个类实现 Serializable
接口时,表示该类的对象可以被序列化。
问题:作为一个空接口,那我不implements它,别人是怎么知道并报出对应错误的?
答:在执行序列化时,JVM会使用反射机制来检查一个类是否实现了 Serializable
接口。如果类没有实现 Serializable
接口,序列化过程会直接失败,抛出 NotSerializableException
。这就是JVM能够知道一个类是否实现了 Serializable
的原因。
transient 关键字
- transient 是Java的关键字,表示某个字段不参与序列化。即当对象被序列化时,
transient
修饰的字段不会被序列化到字节流中。 - 当对象反序列化时,
transient
字段会被设置为默认值(例如null
、0
、false
等)。
用法
class User implements Serializable {String username;transient String password; // 不序列化此字段
}
多线程
JUC是什么:即java.util .concurrent,在JDK1.5引入。是一个处理线程的工具包。
视频后面还提到了多线程,对于刚做完C++中webServer
项目的我,感觉很熟悉hhh。 像视频中提到的什么线程池这些概念,都是认识的,只不过Java写的方式不太一样罢了。
重要的是基础,对于代码层面的,以后需要接触再速成一下即可。
视频中间还有网络编程和GUI,都不是重点内容,就先跳过了
注解和反射
注解(Annotation)
JDK5 时引入的新特性。注解是Java提供的一种元数据机制,用于为代码中的类、方法、字段等元素提供附加信息。在Java中,注解不会直接影响代码的运行逻辑,而是为编译器或其他工具提供额外信息。
元注解
元注解是用于修饰其他注解的注解,例如@Retention
, @Target
等,用来定义注解的作用范围和生命周期。
- Target注解的作用是:描述注解的使用范围
- Reteniton注解的作用是:描述注解保留的时间范围
内置注解
Java中预定义的一些注解,如@Override
(表示方法重写)、@Deprecated
(标记过时的方法)等。
自定义注解
用户可以定义自己的注解,指定注解的目标和生命周期。自定义注解通过@interface
关键字定义。
@Retention(RetentionPolicy.RUNTIME) //元注解注解我们的自定义注解
public @interface MyAnnotation {
}// 使用注解
@MyAnnotation
class Person{
}
反射(Reflection)
反射是Java的一种动态机制,允许在运行时获取类的结构信息(如方法、构造函数、字段等),并能动态调用方法、构造实例等。反射是Java提供的非常强大的功能,但滥用反射可能导致性能问题,并破坏代码的安全性。
推荐视频:Java中的反射 Reflection in Java
获取Class对象
获取Class对象有三种主要方式
1.Class<User> userClass = User.class
; 在编译时就确定了具体的类,属于静态引用。使用这种方式获得类对象时,不会立即触发类的静态初始化块。
2.User user = new User();
Class<?> clazz = user.getClass();
这个class实在运行时从User实例获取的,而User的具体类型,只能在运行时创建和确定。在编译阶段无法准确判断Class对象的确切类型。因此使用通配符<?>。
3.Class<?> clazz = Class.forName("com.zy.user");
通常用在类名在编译时不可知的场景中。使用这种方式获得类对象时,会触发静态初始化块。
Class对象方法
Class对象有很多方法,这里着重讲几个核心的。
1.Field[] fields = clazz.getDeclaredFields();
这样可以获得类中所有字段(无论是否私有)但是不包括父类的字段;
2.Field[] fields = clazz.getFields();
这样可以获得所有public的字段,包括父类的。
后面很多Declared
字段和不包括Declared
字段的都是一样的区别。
3.Field fields = clazz.getField("name");
可以获得指定变量。
4.getDeclaredAnnotation(MyAnnotation.class)
获得注解。
5.获得类中的值
Class<?> clazz = Class.forName("com.zy.User");Field field = clazz.getDeclaredField("publicStaticField");field.setAccessible(true); // 为了访问私有字段System.out.println(field.get(null)); //这里调用的是静态方法,不依附于任何实例,所以用null
6.获得所有方法
Class<?> clazz = Class.forName("com.zy.User");Method[] methods = clazz.getDeclaredMethods();for(Method method : methods) {System.out.println(method.getName());}
7.执行方法 invoke
Class<?> clazz = Class.forName("com.zy.User");Method method = clazz.getDeclaredMethod("publicStaticMethod");method.invoke(null); // 同样这里是静态方法
8.执行带参数的方法
Class<?> clazz = Class.forName("com.zy.User");Method method = clazz.getDeclaredMethod("publicStaticMethod",String.class); //带String 参数method.invoke(null,"Hi here"); //调用时需要传入String 参数
9.获取构造器创建对象
Class<?> clazz = Class.forName("com.zy.User");//获取无参构造器
// Constructor<?> constructor = clazz.getDeclaredConstructor();//获取有参构造器Constructor<?> constructor = clazz.getDeclaredConstructor(String.class, int.class);Object obj = constructor.newInstance("AAA",12);if(obj instanceof User) {User user = (User)obj;}
推荐的java反射视频中,最后有举个例子,利用反射做个小框架,建议看看。