学校老师的Java课水的离谱,我在B站找到关于Java的视频并参考Java编程思想这本书来自学Java。这篇文章主要记录了视频学习,较为浅显且内容不多,后面会把更加深入的学习另外记录下来。
还在更新中…
Java基础
Java分为三大平台
Java SE | Java EE | Java ME |
---|---|---|
标准版 | 企业版 | 微型版 |
桌面应用 | web应用 | 手机应用 |
Java运行环境JRE=JVM(虚拟机)+API(库) | ||
JDK(开发工具包)=JRE+一些工具 |
Java安装
在Java官网下载JDK,开始的时候我安装在桌面发现总是安装失败,发现原因是安装路径不能有中文,而我的桌面路径是G:\桌面,因此更换安装路径即可解决。
然后配置环境变量,控制面板→系统和安全→系统→高级系统设置→环境变量。在用户变量中新建一个变量,变量名为JAVA_HOME,变量值为JDK的路径,如E:\Java。然后在path中新建环境变量%JAVA_HOME%\bin,两个%表示引用。设置该环境变量的好处是在配置eclipse或VScode的环境时,会变得简单。
之后安装好VScode的相关扩展即可。
与C++的比较
finalize()
首先明确一点,finalize()与C++中的析构函数是不同的。在C++中对象一定会被销毁,而Java的对象却并非总被垃圾回收。即:1.对象可能不被垃圾回收;2.垃圾回收不等于析构。例如:一个图形对象在创建时将自己绘制到屏幕上,如果不调用方法将其擦除,它可能可能永远得不到清理(相比较下C++在对象被析构时一定会被清理)。而如果在finalize()里加入擦除的功能,当垃圾回收发生时(不保证一定会发生,如果JVM没有面临内存耗尽的情形,它不会浪费开销取执行垃圾回收)就会被正确清理。
当对象并非通过new来获得了一块特殊的内存时,就不能由垃圾回收器回收其内存,此时就需要使用finalize()方法。当垃圾回收器准备释放对象的储存空间时,会首先调用其finalize()方法,并在下一次垃圾回收时才真正收回其内存。
这就引出了一个问题,既然Java中万物皆对象,那么什么时候会发生通过创建对象方式以外的其他方式为对象分配内存呢?这种情况发生在使用本地方法的时候。本地方法是一种在Java中调用非Java代码的方式,也许会在这里调用C的malloc()函数分配储存空间。
多态
Java使用后期绑定的概念。当向对象发送消息时,编译器只确保方法存在,被调用的代码在运行时确定。为了执行该功能,Java使用一小段特殊代码来替代地址,该段代码使用在对象中存储的信息来计算方法的地址。Java除了static和final(private方法属于final方法)之外,其他所有方法都是后期绑定的。相比较下,C++在默认情况下并不是动态绑定的,除非其通过virtual
来声明。
泛型
C++和Java在模板/泛型的实现是不同的,会出现一些细微的差别。具体的差别我记在别的文章里,就不占用这里的地方了。
内存划分
- 栈
存放方法中的局部变量,对象的引用。方法一定在栈中运行 - 堆
存放所有对象,堆中的数据都有默认值,有一个16进制的地址。与栈的不同是编译器不需要知道存储的数据在堆中存活多久。
字符串常量也存放在堆中。 - 方法区
存放.class相关信息,包含方法的信息 - 本地方法栈
与操作系统有关 - 寄存器
最快的存储区,与CPU有关。(C++允许声明寄存器的分配方式)
嵌入式文档
javadoc是JDK的一部分,用于提取注释,输出一个HTML文件。javadoc命令由/**开始,以*/结束,每行都由*开始。主要有两种使用方式:
- 嵌入HTML
不要在嵌入HTML中使用标题标签,因为javadoc会自己插入标题。 - 文档标签
- @author:作者信息
- @version:版本说明
- @see:引用其他类的文档
- @param:对方法参数的说明
- @return:对方法返回值的说明
注意,javadoc只能为protected和public成员进行文档注释。
Java学习
最多只能有一个类声明为public,且该类名必须于源文件名相同。
变量
byte型变量是一个字节的整型变量。
char型变量不同于C/C++,占两个字节,Java的字符集是Unicode而不是ASCII码。
boolean表示C++中的bool型。
输入输出
print和println,后者自带换行符。
用java.util.Scanner类输入
import java.util.Scanner;
public class ScannerTest{
public static void main(String[] args){
Scnanner scanner = new Scanner(System.in);
System.out.print("输入一个数");
int a = scanner.nextInt();
scanner.close();
}
}
字符串
所有对字符串的修改实际上都是创建一个新的String对象。
首先明确对于引用类型,== 比较的是地址。对于基本类型,==才比较值。
//声明字符串
public static void main(String[] args) {
//方法1
String str1 = "aaa";
String str2 = "aaa";
//方法二
char[] array = {'a', 'a', 'a'};
String str3 = new String(array);
//方法三
String str4 = new String();
str4 = "aaa";
//比较
System.out.println(str1 == str2); //true
System.out.println(str1 == str3); //false
System.out.println(str1 == str4); //true
System.out.println(str3 == str4); //false
}
对于方法一:先将字符串常量"aaa"存放在堆中的字符串常量池,str1存放在栈中,指向一个存放在堆中的字符串对象,该对象指向"aaa"。声明str2时,因为已经有了"aaa",故只需将str2指向与str1相同的对象即可。
因此str1 == str2会输出true。
对于方法二:首先堆中的char[]会被转化为byte[],然后在堆中创建一个新的对象指向该新的地址,最后将str指向该对象。
因此str1 == str3会输出false
上述情况的图示
String str = null;
System.out.println(str.equals("aaa")); //报错:空指针异常
System.out.println("aaa".equals(str)); //输出false
正则表达式
在其他语言中,\ \ 表示:我想要在正则表达式中插入一个普通的(字面上的)反斜杠,不要给它任何特殊的意义。
在 Java 中,\ \ 表示:我要插入一个正则表达式的反斜线,所以其后的字符具有特殊的意义。
一些关键词
在方法内定义变量时,不能使用初final外的其他修饰词。
- final
final修饰类时称为最终类,不能被其他类继承。其中的方法默认为final方法。
final修饰的方法只能被继承使用,而不能被子类重定义。
final修饰的数据成员只可以在声明时进行初始化。 - static
static方法可以通过类名来调用。
static方法只能访问static方法,但非static方法可以访问static方法。
static方法不能被覆盖,即该类子类不能有相同名和相同参数的方法。
//静态代码块
static {
//内容
}
静态代码块的内容总是最先执行,当第一次运行到本类时,静态代码块执行唯一的一次。
- abstract
抽象方法:留给子类实现的方法,()后直接分号。public abstract void method();
抽象类:含有抽象方法的类就是抽象类。抽象类中可以没有抽象方法,但有抽象方法的就是抽象类。 - finally
在异常中无论catch到哪种类的异常都需要被执行的代码块(如用于处理资源释放的代码),就要写在最后一个catch后的finally块中。
权限修饰符
在下面的情况下能否访问到,1表示可以,0表示不可以。
情况 | public | protected | 缺省 | private |
---|---|---|---|---|
同一个包 | 1 | 1 | 1 | 0 |
不同包的子类 | 1 | 1 | 0 | 0 |
不同包且非子类 | 1 | 0 | 0 | 0 |
继承
继承的基础语法为:class Derived extends Base{}
- Java继承的三个特点
- Java是单继承的,不允许多继承,即不允许一个子类继承自多个父类
- 可以多级继承。
- 一个父类可以有多个子类。
通过方法访问父类和子类的同名成员变量时,该方法属于谁,就访问的是谁的成员变量。
@override
用于显式地声明该方法是重写的,类似于C++中的override关键词
//子类重写method()方法
@Override
public void method() {
//内容
}
子类的重写方法权限必须大于等于父类。如:父类为protected时,子类不能为private。
super的三种用法
- 1.在子类的方法中,访问父类的成员变量。
super.num
- 2.在子类的方法中,访问父类的成员方法。
//super的第二种用法,可以提高代码复用率 //父类 class OneSound { public void sound() { System.out.println("a"); } } //子类 class TwoSound extends OneSound { public void sound() { //子类的sound()方法要将功能更新为输出"ab" //一般的更新办法: System.out.println("a"); System.out.println("b"); //如果输出a的功能实现需要很多代码,在这里重写一遍会极大的降低代码的复用率,此时即可使用super //更好的更新办法: super.sound(); System.out.println("b"); } }
- 3.在子类的构造方法中,访问父类的构造方法。
子类的构造方法中有一个默认的super(),用于调用父类的无参构造方法。可以通过super(parameter, …)来调用父类的有参构造方法,但只能位于子类构造方法的第一句,且只能使用一次。
接口
接口就是多个类的公共规范,声明语句为public interface 接口名 {}
其中可以包含的内容有:
- 常量
声明格式为public static final
,必须初始化赋值。一般该数据成员的命名格式为全大写+_
- 抽象方法
接口中方法的默认类型,声明语句为public abstract
,实现类中必须覆写所有的抽象方法,除非是抽象类。
- 默认方法
为了实现接口的升级问题,即当接口需要增加新功能而又不希望所有的实现类都因此而改动时,就要用默认方法。实现类可以不覆写默认方法,会直接继承于其接口。public default
- 静态方法
不能用实现类调用接口的静态方法,只能通过接口名调用。同样的,如果要创建某些公共代码供接口的所有不同实现公用,就可以使用接口内部的嵌套类。
- 私有方法
- 普通私有方法
解决多个默认方法之间代码重复的问题。private
- 静态私有方法
解决多个静态方法之间代码重复的问题。private static
- 普通私有方法
接口不能直接使用,必须有一个实现类来实现接口,类比继承的extends
,实现接口使用implements
虽然类不允许多继承,但允许接口多继承,也允许一个实现类implement多个接口。
如果实现类实现的多个接口中有重复的相同抽象方法,那么实现类只需覆写一次即可。如果有重复的相同默认方法,那么实现类必须对其进行覆写。
一个类如果其父类的方法和其接口的方法冲突,则优先使用的是父类的方法。
关于什么时候使用抽象类,什么时候使用接口。Java编程思想 中说应该优先选择类而不是接口,如果接口的必需性非常明确再去选择。这篇文章讲的蛮清楚。
多态
对象的向下转型,前提是在new的时候已经进行了向上转型,如下:
Base c1 = new Devided();
Devided c2 = (Devided) c1;
使用instanceof
来判断一个父类引用的对象是由什么子类转型而来的。返回一个boolean值,判断前面的对象能否当作后面类的实例。
if(c1 instanceof Devided)
Devided c2 = (Devided) c1;
泛型
//泛型类
public class Generic <T> {}
//泛型方法
public <T> void method() {}
与泛型类相比,尽量使用泛型方法
?是泛型的通配符,可以有如下语法:
//泛型的上限,此时?必须是Number或其子类
public static void method(ArrayList<? extends Number> list){}
//泛型的下限,此时?必须是Number或其父类
public static void method(ArrayList<? super Number> list){}
//Integer 继承自 Number 继承自 Object
T和?的区别
以下两者都可以运行
public static <T> void method(ArrayList<T> list) {
Iterator<T> i = list.iterator();
while (i.hasNext()) {
Object o = i.next();
System.out.println(o);
}
}
public static void method2(ArrayList<?> list) {
Iterator<?> i = list.iterator();
while (i.hasNext()) {
Object o = i.next();
System.out.println(o);
}
}
?和 T 都表示不确定的类型,区别在于我们可以对 T 进行操作,但是对 ?不行,即
T t; //正确的语法
? t; //错误的语法
T 是一个 确定的 类型,通常用于泛型类和泛型方法的定义,?是一个 不确定 的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法
自限定类型
自限定类型class A<T extends A<T>>
用以保证类型参数必须与正在被定义的类相同。
在不使用自限定类型时
class Basic{
Basic b;
public void set(Basic b){ this.b = b; }
}
class SubType extends Basic{
SubType b;
public void set(SubType b){ this.b = b; }
}
SubType st = new SubType();
st.set(new SubType());
st.set(new Basic()); //调用了继承自Basic的set方法
SubType没有覆写Basic的set函数,因此一直保留着父类的接口。而自限定类型可以解决这个问题:
class Basic<T extends Basic<T>>{
T b;
public void set(T b){ this.b = b; }
}
class SubType extends Basic<SubType>{}
不仅增加了代码的复用率,而且SubType类也不再能调用Basic的set()方法。自限定类型可以保证SubType对象只与SubType对象交互,而不可以和其他类型参数的对象交互。
参考了这篇文章。
内部类
在类内部再声明另一个类,内部类可以访问其外部类的任意成员,它拥有外部类所有元素的访问权。这与C++的嵌套是不同的,C++的嵌套只是单纯的名称隐藏。外部类必须通过内部类的对象访问。
class Outer {
private int x = 10;
class Inner {
private int y = 20;
}
}
必须使用外部类的对象来创建内部类的对象,在拥有外部类对象之前是不能创建内部类对象的。因为内部类对象在创建时会连接到创建它的外部类对象上,即保存一个引用,指向创建它的外部类对象。使用.new
语法。
//用临时对象声明一个内部类的对象
Outer.Inner in = new Outer().new Inner()
//用已有对象声明一个内部类对象
Outer out = new Outer();
Outer.Inner in = out.new Inner();
如果不需要内部类对象与其外部类对象只间有联系,那么可以将内部类声明为static,这称为嵌套类。因为嵌套类并不连接到其外部类,因此就不需要外部类对象来创建它,它也不能访问外部类的非静态对象。
由于内部类和嵌套类的不同,普通的内部类就不能有static数据了。
局部内部类
局部内部类即在方法的作用域内创建的类。局部内部类如果要访问该方法的局部变量,则该变量必须是final的(或者不写final,但不更改其值)。
class Outer {
public void methodOut() {
//方法的局部变量
final int x = 10; //这里的final不写也可以,但不能有更改x的语句
//方法的内部类
class Inner {
public void methodIn() {
//访问该方法的局部变量x
System.out.println(x);
}
}
Inner in = new Inner();
inner.methodIn();
}
}
public class OutAndIn {
public static void main(String[] args) {
Outer o = new Outer();
o.methodOut(); //输出10
}
}
原因如下:
调用methodOut()时,首先是methodOut()进栈,x进栈。然后在堆中创建对象in。最后methodIn()进栈、出栈,x出栈,methodOut()出栈。此时methodOut()中的x的生命周期已经结束了,然而堆中的对象in要在最后垃圾回收时才会死去,因此in可能会访问到的x就不允许被更改。Java会把x拷贝一份给in来解决生存周期的问题。
匿名内部类
new Base() {//匿名内部类的声明};
表示创建一个继承自Base的匿名类对象,在new所返回的对象被自动的向上转型为Base的引用。
有时不想为了使用接口专门写一个接口的实现类,即可能接口的实现类(或者父类的子类)只使用一次。此时就可以用到匿名内部类。
//错误写法,因为没有实现类
MyInterface obj = new MyInterface();
//匿名内部类
MyInterface obj = new MyInterface() {
//覆写接口中的所有抽象方法
};
I\O
输入流 | 输出流 | |
---|---|---|
字节流 | InputStream | OutputStream |
字符流 | Reader | Writer |
File类
File类能代表一个文件或一个目录。
- list()方法以String[]的形式返回此File对象包含的全部列表
File f = new File("G:\desktop");
String[] list = f.list();
for(String l : list) {
System.out.println(l);
}
- listFiles():同list(),只是返回的类型是File[]。如果要指定需要的文件,如只要.java文件,就需要实现FileFilter或者FileNameFilter接口1。
这两个接口都只有一个accept()方法,用于判断是否是需要的文件。
import java.io.File;
import java.io.FileFilter;
//自己实现一个根据需求的FileFilter的实现类
class FileFilterJava implements FileFilter {
@Override
public boolean accept(File file) {
//只需要以".java"结尾的文件
if(file.toString().endsWith(".java")) {
return true;
}
else
return false;
}
}
public class Test {
//输出所有.java文件的名字
public static void getJava(File dir) {
File[] files = dir.listFiles(new FileFilterJava());
for(File f : files) {
System.out.println(f.getName());
}
}
public static void main(String[] args) {
File java = new File("G:\\desktop\\Documents\\Java\\Others\\Test\\src");
getJava(java);
}
}
- exists():此对象表示的文件或目录是否存在
- isDirectory():此对象是否为目录
- isFile():此对象是否为文件
- createNewFile():创建该对象的文件,如果已存在则不创建
- mkdir():创建该对象表示的目录(单级文件夹)
File f = new File("G:\\desktop\\a\\b"); //若没有a文件夹则不创建,返回false
- mkdirs():创建该对象表示的目录(包括需要的多级文件夹)
File f = new File("G:\\desktop\\a\\b"); //哪怕没有a文件夹也会创建a,b文件夹
- delete():删除该对象表示的文件或目录,若删除的目录内有其他文件或目录则不会删除
File f = new File("G:\\desktop\\a\\b");
System.out.println(f.mkdirs()); //输出true
File f1 = new File("G:\\desktop\\a");
System.out.println(f1.delete()); //输出false
FileOutputStream和FileInputStream类
构造方法的参数可以是路径也可以是一个File对象。
如果要在文件中追加输出(即不覆盖文件),则需要第二个参数
FileOutputStream fos = new FileOutputStream(file, true);
反之如果要覆盖之前的输出则将true改为false。
- close():关闭此输出流并释放其相关资源。
- flush():刷新此输出流并强制任何缓冲的输出字节被写出。
- write(byte[] b):将字节数组b写入此输出流。
- write(byte[] b, int off, int len):从偏移量off开始将字节b写出len个字节。
要写入String时,调用String类的getBytes()方法将其转化为二进制字节再写入。
FileInputStream类似。
- read():读取下一个字节。读到文件末尾时返回-1。
- read(byte[] b):读取字节存储到b中,返回读取到的字节数。
String类有一个构造方法为String(byte[] b)
,即将字节数组b转化为字符串。
Writer和Reader类
和上面的两个类方法一样,只是针对的是字符操作。因此write()方法多了几个能写字符串或字符数组的重载。
OutputStreamWriter和InputStreamWriter类
OutputStreamWriter类继承了Writer类,它可以使用指定的编码格式输入。
构造方法为OutputStreamWriter(OutputStream out, String charsetName)
,第一个参数为输出流,第二个参数为指定的编码格式,默认为UTF-8。
InputStreamWriter类类似。
ObjectOutputStream和ObjectInStream类
将对象以流的方式写入到文件叫做对象的序列化,反之,读取叫做反序列化。
ObjectOutputStream继承了OutputStream类。
用WriteObject(Object object)
将对象写入。注意要写入的类必须是Serializable
接口的实现类(没有任何需要覆写的方法,该接口相当于一个允许序列化和反序列化的标识)。如果要实现对序列化过程的控制就要实现Externalizable
接口(它继承了Serializable接口),它有两个方法:writeExternal()
和readExternal()
会在序列化和反序列化时被调用。
transient
关键字可以关闭对象某个字段的序列化。例如:一个Login对象保存一个登录的信息,我们想把登录信息存储下来,但不包括密码。
import java.io.*;
import java.util.Date;
class Login implements Serializable {
private Date date = new Date();
private String username;
//不希望保存密码
private transient String password;
public Login(String name, String psw) {
username = name;
password = psw;
}
public String toString() {
return "login info:\n\tusername: " + username + "\n\tdate: " + date + "\n\tpassword " + password;
}
}
public class Transient {
public static void main(String[] args) throws Exception {
Login login = new Login("Arisa", "Florentinal");
//输出login的信息
System.out.println(login);
//将对象login序列化保存到login.txt中
ObjectOutputStream ops = new ObjectOutputStream(new FileOutputStream("login.txt"));
ops.writeObject(login);
ops.close();
//反序列化
ObjectInputStream ips = new ObjectInputStream(new FileInputStream("login.txt"));
login = (Login)ips.readObject();
System.out.println(login);
}
}/*输出为:
login info:
username: Arisa
date: Sun Apr 25 17:14:34 CST 2021
password Florentinal
login info:
username: Arisa
date: Sun Apr 25 17:14:34 CST 2021
password null*/
流对象的异常处理
//在try的()中定义流对象
try() {
//try块中执行操作
} catch() {
//捕获处理异常
} finally {
//一般在finally中调用close()
}
类型信息
运行时类型信息RTTI(Run-Time Type Identification)使得我们可以在程序运行时发现和使用类型信息。
Java通过两种方式让我们在运行时识别对象和类的信息:一是传统的RTTI,它假定我们在编译时已经知道了所有的类型;二是反射机制,允许我们在运行时发现和使用类的信息。
RTTI
Class对象包含于类有关的信息,用来执行RTTI。用Class类的各种方法获得类信息。
Java程序在它运行开始之前并非被完全加载,各个部分都是在必需时才加载的。这与C++这种静态加载语言是不同的。JVM使用被称为类加载器的子系统来动态加载各个类。类加载器首先检查这个类的Class对象是否已经加载,如果尚未加载,默认的类加载器就会根据类名查找.class文件。在这个类的字节码被加载时,它们会接受验证以确保没有被破坏且不包含不良代码。一旦某个类的Class对象被载入内存,它就被用来创建这个类的所有对象。
以下三种方法都可以获得类的Class对象。
- getClass()是Object类的方法,返回Class对象。
- forName()是Class类的方法,其参数是要类名,返回class对象。
- 类字面常量是通过:类.class返回class对象。
关于类型检查,instanceof指的是“A是B类或B的子类吗”,而两个Class对象引用的==或equals()方法指的是“A是B类吗”。即前者考虑了继承的关系,而后者没有。
当使用.class来创建对Class对象的引时,不会自动地初始化该Class对象。为了使用类而做的准备工作包含三个步骤:
- 加载。类加载器找到字节码并创建一个Class对象。
- 链接。验证字节码,为静态域分配存储空间。如果需要的话,将解析这个类创建的对其他类的所有引用。
- 初始化。如果该类具有超类,则对其初始化。初始化被延迟到了对静态方法(构造器是隐式的静态方法)或者非常数静态域进行首次引用时才执行。
反射
反射(reflect)就是将类的各个组成部分封装为其他对象。它是一种运行时的类信息,可以在运行时去操作其他对象。通常不需要直接使用反射,它是用来支持其他特性,在创建更加动态的代码时会很有用的一种机制。
RTTI和反射的真正区别在于:对RTTI来说,编译器在编译时打开并检查.class文件。而对于反射机制来说,.class文件在编译时是不可获取的(如通过网络获得),所以是在运行时打开并检查它。
用Field类操作成员变量,用Constructor类操作构造函数,用Method类操作方法。
如果要访问private成员变量,需要设置其为可访问:
// 通过Field类获得成员变量
Field field = SomeClass.getDeclearField("member"); // member是SomeClass类一个成员变量的名字
// 如果要访问或者修改它,需要先设置其为可访问(当其为private时)
field.setAccessible(true); // 暴力反射
// 然后就可以访问它
Object value = field.get();
注解
Java目前内置了三种注解和四种元注解。
内置注解
注解 | 作用 |
---|---|
@Override | 当前的方法将覆写其超类的方法 |
@Dsprecated | 如果使用了被该注解注解了的元素编译器会报错 |
@SuppressWarnings | 关闭不当的编译器警告 |
@Test | 标明是测试方法,可以单独执行(而不在main方法中执行) |
@Before | 所有测试方法执行前必须执行的方法 |
@After | 所有测试方法执行完后执行的方法 |
自定义注解
元注解用来注解自定义注解:
- @Target:表示该注解用于什么地方,可能的ElementType参数包括:
CONSTRUCTOR:构造器的声明
FIELD:域声明
LOCAL_VARIABLE:局部变量声明
METHOD:方法声明
PACKAGE:包声明
PARAMETER:参数声明
TYPE:类、接口或enum声明 - @Retention:表示需要在什么级别保存该注解信息,可能的RetentionPolicy参数包括:
SOURCE:注解被编译器丢弃
CLASS:注解在class文件中可用
RUNTIME:在运行期也保留注解,因此可以通过反射机制读取注解的信息 - @Documented:将此注解包含在Javadoc中。
- @Inherited:允许子类继承父类的注解。
自定义注解中可以定义成员方法,称为其属性。属性的返回类型必须是基本类型(或enum)。定义属性时可以用default
声明其默认值,在使用注解时就不需要为其赋值了。自定义注解的格式为:
@Target(ElementType.Method)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
//注解属性,如
public int id() default -1;
}
并发
并发通常是提高运行在单处理器上的程序的性能,实现并发最直接的方式是在操作系统级别使用进程。进程是运行在它自己的地址空间内的自包容的程序。一个线程就是在进程中的一个单一的顺序控制流,单个进程可以拥有多个并发执行的任务。
实现Runnable接口并覆写run()方法,来定义一个任务。用Thread类来创建线程,用Executor来管理Thread对象。
Runnable只执行任务,如果需要任务完成时能够返回一个值,则需要实现Callable接口。
synchronized关键字
共享资源一般是以对象形式存在的内存片段,也可以是文件、输入输出端口等。要控制对共享资源的访问,得先把它包装进一个对象。然后把所有要访问的这个资源的方法标记为synchronized。
如果某个任务处于一个对标记为synchronized的方法的调用中,那么在这个线程从这个方法返回之前,其他所有要调用类中标记为synchronized的方法的线程都会被阻塞。
synchronized(锁对象) {
//需要访问共享数据的代码
}
注意:在使用并发时,将域设置为private是非常重要的,否则,synchronized就不能防止其他任务直接访问域。
挂起
使用wait()将任务挂起,直到notify()或notifyAll()发生时被唤醒。在wait()期间对象的锁是被释放的。
wait()有两个重载:无参形式将无限等待下去直至接收到notify()或notifyAll()信号;也可以接受一个毫秒数作为参数,在接收到信号,或时间到时被唤醒。
特殊的是:wait(),notify()和notifyAll()都是基类Object的一部分,而非Thread类的一部分。
死锁
考虑一个经典的死锁问题哲学家就餐 :五个哲学家围成一圈或者思考,或者就餐。思考不需要任何资源,就餐时需要两根筷子才能吃饭。但他们每人只拿了一根筷子,并将其一一放在每两个人之间。
当以下四个条件同时满足时,就会发生死锁
- 互斥条件。任务使用的资源中至少有一个是不能共享的。如上例的一根筷子一次只能被一个哲学家使用。
- 至少有一个任务它必须持有一个资源且正在等待一个当前被别的任务持有的资源。如上例的哲学家拿着一根筷子等待另一根筷子。
- 资源不能被任务抢占。如上例的哲学家不能抢别人的筷子。
- 必须有循坏等待。如上例的哲学家都拿右手的筷子等左手的筷子。
也因此要防止死锁,只需要选在其中之一容易破坏的条件即可。
Lambda表达式
Lambda表达式是Java中的函数编程思想的体现。有时候我们只想做一件事,而不想为了做这件事专门声明一个对象,这时就可以使用Lambda表达式。
有且仅有一个抽象方法的接口叫做函数式接口,Lambda表达式必须用于函数式接口。
格式为(参数列表) -> {方法体}
{}里写覆写接口的抽象方法的方法体,()里写接口中抽象方法的参数列表。
()中的参数数据类型可以省略,若只有一个参数则数据类型和()都可以省略不写。
{}中如果只有一行代码,则{}、return都可以省略不写。
ArrayList类
ArrayList<String> list = new ArrayList<>();
//当ArrayList对象为空时,默认输出是[]
System.out.println(list); //输出为[]
list.add("banana");
System.out.println(list); //输出为[banana]
//几个常用的方法
public boolean add(E e) //添加
public E get(int index) //获取
public E remove(int index) //删除
public int size() //包含元素个数
二者功能是一样的,但在参数上不同,且不对文件进行操作,只筛选的话从效率上说,FilenameFilter是更快的。如果需要对文件进行操作,则FileFilter更加方便。 ↩︎