Java学习笔记
常用命令
单文件编译运行
java main.java编译
javac main运行类文件
有包名编译运行时需加上包名,编译时使用-encoding可以设置编码,在src目录编译运行。
javac com\itranswarp\learnjava\Main.java && java com.itranswarp.learnjava.Main
javac -encoding UTF-8 com\itranswarp\learnjava\Main.java && java com.itranswarp.learnjava.Main
在markdown中使用代码展开
代码展开
这里填入代码语法
基本语法
数据类型
byte: -128~127
布尔类型boolean
数组类型int[]
int[] ns = new int[5];
int[] ns = new int[] { 68, 79, 91, 85, 62 };
var关键字
类似C++的auto自动定义变量
var sb = new StringBuilder();
实际上会自动变成:
StringBuilder sb = new StringBuilder();
输出
double d = 3.1415926;
System.out.printf("%.2f\n", d);输入
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in); // 创建Scanner对象
System.out.print("Input your name: "); // 打印提示
String name = scanner.nextLine(); // 读取一行输入并获取字符串
System.out.print("Input your age: "); // 打印提示
int age = scanner.nextInt(); // 读取一行输入并获取整数
System.out.printf("Hi, %s, you are %d\n", name, age); // 格式化输出
}
}类型
基本类型:byte,short,int,long,boolean,float,double,char;
引用类型:所有class和interface类型。
引用类型判等要用.equals(),=相当于检查是否指向同一个对象。
面向对象
继承
继承关键字:extends
在Java中,没有明确写extends的类,编译器会自动加上extends Object。
Java只允许一个class继承自一个类。
super关键字表示父类(超类)。子类引用父类的字段时,可以用super.fieldName
子类构造函数调用父类的构造函数:
class Student extends Person {
protected int score;
public Student(String name, int age, int score) {
super(name, age); // 调用父类的构造方法Person(String, int)
this.score = score;
}
}向上转型:把一个子类型安全地变为更加抽象的父类型:
Person p = new Student();
override(C++的重载)
在继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为覆写(Override)。
加上@Override可以让编译器帮助检查是否进行了正确的覆写。希望进行覆写,但是不小心写错了方法签名,编译器会报错。
abstract
通过abstract定义的方法是抽象方法,它只有定义,没有实现。抽象方法定义了子类必须实现的接口规范;
定义了抽象方法的class必须被定义为抽象类,从抽象类继承的子类必须实现抽象方法.
abstract class Person {
public abstract void run();
}interface(接口)
如果一个抽象类没有字段,所有方法全部都是抽象方法:
就可以把该抽象类改写为接口:interface
Java的接口特指interface的定义,表示一个接口类型和一组方法签名,而编程接口泛指接口规范,如方法签名,数据格式,网络协议等。
一个类可以实现多个interface
class Student implements Person, Hello { // 实现了两个interface
...
}包作用域
package
包作用域是指一个类允许访问同一个package的没有public、private修饰的class,以及没有public、protected、private修饰的字段和方法。
final
用final修饰class可以阻止被继承:
package abc;
// 无法被继承:
public final class Hello {
private int n = 0;
protected void hi(int t) {
long i = t;
}
}用final修饰method可以阻止被子类覆写:
package abc;
public class Hello {
// 无法被覆写:
protected final void hi() {
}
}用final修饰field可以阻止被重新赋值:
package abc;
public class Hello {
private final int n = 0;
protected void hi() {
this.n = 1; // error!
}
}用final修饰局部变量可以阻止被重新赋值:
package abc;
public class Hello {
protected void hi(final int t) {
t = 1; // error!
}
}内部类inner Class
// inner class
public class Main {
public static void main(String[] args) {
Outer outer = new Outer("Nested"); // 实例化一个Outer
Outer.Inner inner = outer.new Inner(); // 实例化一个Inner
inner.hello();
}
}
class Outer {
private String name;
Outer(String name) {
this.name = name;
}
class Inner {
void hello() {
System.out.println("Hello, " + Outer.this.name);
}
}
}Inner Class除了有一个this指向它自己,还隐含地持有一个Outer Class实例,可以用Outer.this访问这个实例。所以,实例化一个Inner Class不能脱离Outer实例。
Java的编译运行过程(简单):
class,classpath,模块等见参考资料3.12-3.14
Java包装类型
包装类型是基本类型相对应的引用类型。如int对应的引用类型是java.lang.Integer,可直接使用。
Javabean
JavaBean是一种符合命名规范的class,它通过getter和setter来定义属性;
我们通常把一组对应的读方法(getter)和写方法(setter)称为属性(property);
使用Introspector.getBeanInfo()可以获取属性列表。(先import java.beans.*;)
异常处理
public byte[] getBytes(String charsetName) throws UnsupportedEncodingException { ... }
在方法定义的时候,使用throws Xxx表示该方法可能抛出的异常类型。调用方在调用的时候,必须强制捕获这些异常或者同样声明可能抛出异常,否则编译器会报错。
- 待扩展
反射
JVM为每个加载的class及interface创建了对应的Class实例来保存class及interface的所有信息;获取一个class对应的Class实例后,就可以获取该class的所有信息;通过Class实例获取class信息的方法称为反射(Reflection)。
可以通过反射获取类的字段(Field)、方法(Method)、构造方法和继承关系,可通过传入类的具体实例得到对应的具体信息。
反射是为了解决在运行期,对某个实例一无所知的情况下,如何调用其方法。
动态代理
...
注解
编译检查
@OverRide - 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
@deprecated - 标记过时方法。如果使用该方法,会报编译警告。
@SuppressWarnings - 指示编译器去忽略注解中声明的警告。
通过反射获取自定义注解
...
多线程
创建线程
Java用Thread对象表示一个线程,通过调用start()启动一个新线程;start()方法会在内部自动调用实例的run()方法。
可以通过Thread派生类或者实现Runnable接口的类来创建线程(覆写类run()方法)。
Thread.sleep()暂停线程,单位为ms;Thread.setPriority(int n)设置线程调度优先级。
通过对另一个线程对象调用join()方法可以等待其执行结束,例如t.join();
中断线程
对目标线程调用interrupt()方法可以请求中断一个线程,目标线程通过检测isInterrupted()标志获取自身是否已中断。如果目标线程处于等待状态,该线程会捕获到InterruptedException。
线程间共享变量需要使用volatile关键字标记,确保每个线程都能读取到更新后的变量值。(volatile解决的是可见性问题)
volatile关键字的目的是告诉虚拟机:
每次访问变量时,总是获取主内存的最新值;
每次修改变量后,立刻回写到主内存。
守护线程
守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。
在调用start()方法前,调用setDaemon(true)把该线程标记为守护线程。
守护线程不能持有需要关闭的资源(如打开文件等)。
线程同步
public synchronized void add(int n) { // 锁住this
count += n;
} // 解锁等价于
public void add(int n) {
synchronized(this) { // 锁住this
count += n;
} // 解锁
}synchronized(this)锁住的是对象,同一个对象里只有一个线程能执行synchronized修饰的代码段,其他线程可以执行非synchronized修饰的代码段。
代码展开
public class Main{
public static void main(String[] args) {
System.out.println("使用关键字synchronized");
Mthreads mt=new Mthreads();
Thread thread1 = new Thread(mt, "mt1");
Thread thread2 = new Thread(mt, "mt2");
Thread thread3 = new Thread(mt, "mt3");
thread1.start();
thread2.start();
thread3.start();
}
}
class Mthreads implements Runnable{
private int count;
public Mthreads() {
count = 0;
}
public void countAdd() {
synchronized(this) {
for (int i = 0; i < 5; i ++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//非synchronized代码块,未对count进行读写操作,所以可以不用synchronized
public void printCount() {
for (int i = 0; i < 5; i ++) {
try {
System.out.println(Thread.currentThread().getName() + " count:" + count);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void printCount2() {
for (int i = 0; i < 5; i ++) {
try {
System.out.println(Thread.currentThread().getName() + " count:" + count);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void run() {
String threadName = Thread.currentThread().getName();
if (threadName.equals("mt1")) {
countAdd();
} else if (threadName.equals("mt2")) {
printCount();
}
else if(threadName.equals("mt3")){
printCount2();
}
}
}这种情况下,mt1和mt3不能同时运行,mt2可以和mt1/mt3同时运行。
我的理解:synchronized(object)修饰代码段相当于让代码段运行前得到object对应的锁,运行后释放锁。
wait和notify
wait()方法在当前获取的锁对象上调用,等待时释放锁,唤醒时试图重新获得锁。
notify()/notifyAll()在锁对象上调用,唤醒一个/所有在该锁对象上等待的线程。
读写锁
ReadWriteLock可以实现读写锁,允许多个线程同时读,但只要有一个线程在写,其他线程就必须等待。(悲观锁)
StampedLock是一种乐观锁,读的过程中允许写锁写入,有小概率导致数据不一致时用悲观锁处理冲突。
Semaphore
Semaphore是信号量,用于限制资源被最多N个线程访问。
semaphore.acquire();获取,semaphore.release();释放。
线程池
ExecutorService接口表示线程池
// 创建固定大小的线程池:
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交任务:
executor.submit(task1);
executor.submit(task2);
executor.submit(task3);
executor.submit(task4);
executor.submit(task5);FixedThreadPool:线程数固定的线程池;
CachedThreadPool:线程数根据任务动态调整的线程池;
SingleThreadExecutor:仅单线程执行的线程池。
放入ScheduledThreadPool的任务可以定期反复执行。
Java面试:为什么不建议使用Executors创建线程池?
Future
Callable接口,和Runnable接口比,它多了一个返回值。
可以用Future获取Callable任务异步执行的结果,获取会在执行完成前阻塞。
ExecutorService executor = Executors.newFixedThreadPool(4);
// 定义任务:
Callable<String> task = new Task();
// 提交任务并获得Future:
Future<String> future = executor.submit(task);
// 从Future获取异步执行返回的结果:
String result = future.get(); // 可能阻塞使用CompletableFuture可以实现回调、线程的串行化、并行化。
ThreadLocal
使用ThreadLocal来设置单个线程独有的上下文(“局部变量”)。
ThreadLocal实例通常总是以静态字段初始化如下:
static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();典型使用如下:
void processUser(user) {
try {
threadLocalUser.set(user);
step1();
step2();
log();
} finally {
threadLocalUser.remove();
}
}
...
User u = threadLocalUser.get();
...ThreadLocal本身不会存储任何数据,ThreadLocal的set方法是将值存储到Thread线程本身的ThreadLocalMap里面了。
真正的项目开发往往会使用线程池,因此线程执行结束时需要清除上下文信息,需要调用ThreadLocoal的remove方法。
虚拟线程
Java 19引入的虚拟线程是为了解决IO密集型任务的吞吐量,它可以高效通过少数线程去调度大量虚拟线程;
虚拟线程在执行到IO操作或Blocking操作时,会自动切换到其他虚拟线程执行,从而避免当前线程等待,能最大化线程的执行效率。
...
函数式编程
FunctionalInterface
单抽象方法接口被称为FunctionalInterface,可用@FunctionalInterface来进行编译检查。
接收FunctionalInterface作为参数的时候,可以把实例化的匿名类改写为Lambda表达式;Lambda表达式的参数和返回值类型均可由编译器自动推断。
FunctionalInterface还可以传入:
符合方法签名的静态方法;
符合方法签名的实例方法(实例类型被看做第一个参数类型);
符合方法签名的构造方法(实例类型被看做返回类型)。
注:上述方法传入时用使用方法引用。
方法引用
例:Main类中静态方法cmp的引用,用Main::cmp表示
You use lambda expressions to create anonymous methods. Sometimes, however, a lambda expression does nothing but call an existing method. In those cases, it’s often clearer to refer to the existing method by name. Method references enable you to do this; they are compact, easy-to-read lambda expressions for methods that already have a name.
Stream
...
网络编程
TCP/UDP/SMTP/http/RMI...
复习网络知识时可扩展...