Java基础
java基础
java的基本程序设计结构
第一个程序
Main.java
public class Main {
public static void main(String[] args) {
System.out.println("hello,world");
}
}
java的源代码的文件名必须和公共类的名字相同,必须以.java
结尾。
- 编译:
javac Main.java
得到二进制文件Main.class
- 执行:
java Main
打印hello world
或者直接java Main.java
执行。
数据类型
基本和C++相同,不同的之处: java中的布尔类型boolean
是不支持数值转换的,即x
为数值,if(x)
这样的代码是不能通过编译的。
变量和常量
变量
java中并不区分变量的声明和定义,且变量声明后必须显式赋值,使用未赋值的语句是错误的,如:
double x;
System.out.println(x);
java 10开始,可根据变量初始化值推断它的类型,只需借助关键字var
: var x = 1.2;
常量
final
关键字指示常量,表示变量只能被赋值一次,之后不能被更改,常量习惯上大写。
public static final double PI = 3.14;
枚举类型
enum Color{RED,BLUE,GREEN};
Color cr = Color.BLUE;
运算符
数学函数
Math
类包含很多数学函数,使用方法Math.funcion()
,其中常用的函数有:
sqrt()
: 开方pow()
:幂运算- 三角函数:
sin, cos, tan, atan...
- 指数函数:
exp, log
,log10
非常接近$\pi$和$e$的常量,分别为Math.PI
和Math.E
。
位运算
算术移位:用符号位填充高位,<<,>>
逻辑移位:用0填充高位,<<<, >>>
字符串
基础
- 子串:
str.substring(i,j)
,str
索引[i, j
)的字符组成的子串 - 拼接:
+
适用于短小且频率不高的字符串拼接,因为效率低,String.join(str1,str2,str3)
效率很高,或者转为StringBuilder
,用对应的append
方法
String s1 = "abcedfg";
String substr = s1.substring(1,3); //bc
String s2 = "a".repeat(3); // aaa java11 支持repeat
String s3 = "a" + "b" + "c"; // aaa
String s4 = String.join("aa","bc","cd"); // aaa
java中字符串的一个重要特点:不可变性,即不能修改字符串中的某个字符,如果非要修改呢?只能让其指向一个新的字符串。不可变性使得字符串间可以共享。JAVA(1)【基础类型】String类型的字符串池 – 小拙 – 博客园 (cnblogs.com)
-
判断是否相等:
s1.equals(s2)
, java中字符串如果用==
判断,实际比较的是地址 -
空串与
null
串: 长度为0的串为空串,null串表示没有任何对象和自己关联,判断一个字符串既不是空串也不是null串:if(s != null && s.length() != 0)
常用api
除了上面的,其他常用的api:
char charAt(int index):
返回给定位置的代码单元empty()
:是否为空startsWith(String prefix)
:是否以prefix作为前缀endsWith(String suffix)
:是否以suffix作为后缀length()
:长度toLowerCase()
/toUpperCase:转小/大写
StringBuilder类
拼接字符串append
的效率高,对应的一些api:
length()
:长度append()
:可添加单个字符或者字符串- setCharAt(int i, char c):将第i个代码单元设置为字符c
insert(int offset, String str/ char c):
在索引offset后插入字符串str或者单个字符cdelete(index1, index2)
:删除索引[index1,index2)之间的字符串,并返回残余的字符串toString():
返回String类型的对象
输入输出
读取输入
Scanner in = new Scanner(System.in);
String s1 = in.nextLine();
String s2 = in.next(); //以空格为分界
int num = in.nextInt(); //in.nextDouble
检查是否还有输入:boolean hasNext()
/ hasNextInt()
/hasNextDouble()
格式化输出
double a = 1 / 3.0;
System.out.printf("%8.2f",a); //总字符长度为8,精确至小数点后2位,即前面留了5个空格
文件输入
String path = "D:/Files/桌面文件/1.txt";
Scanner in = new Scanner(Path.of(path), StandardCharsets.UTF_8);
String s = in.nextLine();
System.out.println(s);
循环
和C++的break
功能更强的是,java中break
语句可以带标签,如多重循环中,break
加上标签可直接跳转到最外层。标签放在循环最外层的前面,并加上冒号。
public static void main(String[] args) {
label:
for(int i=0;i<5;i++) {
for(int j=0;j<5;j++) {
if(j==3) {
break label;
}
System.out.print("i= "+i +" j= " +j + "\n");
}
}
System.out.println("This is end");
}
//输出
i= 0 j= 0
i= 0 j= 1
i= 0 j= 2
This is end
大数
BigDecimal a = BigDecimal.valueOf(100);
BigInteger b = new BigInteger("12466456663875366527162366515632664357");
BigInteger c = new BigInteger("12466456663875366527162366515632664357");
System.out.println(b.add(c));
System.out.println(b.subtract(c)); //减法
System.out.println(b.multiply(c));
System.out.println(b.divide(c));
System.out.println(b.mod(c));
BigDecimal 中除法除不尽会报错,所以除不尽的情况要指定舍去方式:
public class Main {
public static void main(String[] args) {
BigDecimal a = BigDecimal.valueOf(1);
BigDecimal b = BigDecimal.valueOf(3);
System.out.println(a.divide(b, 2, RoundingMode.HALF_UP)); //保留两位小数,并四舍五入,0.33
System.out.println(a.divide(b, 2, RoundingMode.CEILING)); //天花板除法 0.34
System.out.println(a.divide(b, 2, RoundingMode.FLOOR)); //地板除 0.33
}
}
数组
声明数组
public class Main {
public static void main(String[] args) {
int[] a = new int[10];
int [] b = new int[]{1,2,3};
int [] c = {1,4,6};
}
}
数组拷贝
java的数组声明:int[] a = new int[100]
等价于C++里面的int* a = new int[100]
,因此=
赋值实际上只是引用同一个数组,如果想拷贝一份,则应该用Arrays.copyOf()
public class Main {
public static void main(String[] args) {
int [] c = {1,4,6};
int [] d = c;
c[1] = 9;
System.out.println(c);
System.out.println(d);
for(int x:d){
System.out.print(x +"\t");
}
}
}
//
[I@5fd0d5ae
[I@5fd0d5ae
1 9 6
public class Main {
public static void main(String[] args) {
int [] a = {1,4,6};
int [] b = Arrays.copyOf(a,5); //从数组a的起始复制,第二个参数为新数组的总长度,不够补0
a[1] = 9;
System.out.println(a);
System.out.println(b);
for(int x:b){
System.out.print(x +"\t");
}
}
}
//
[I@5fd0d5ae
[I@2d98a335
1 4 6 0 0
用于数组的api
用法:Arrays.
加上下面的api:
-
sort(arr);
采用的是快速排序 -
static String toString(arr):[1, 7, 5, 3, 6, 2, 0]
-
copyOfRange(arr, from, to):
复制arr的区间[from, to)的数据 -
binarySearch(arr, value)
:在有序数组中寻找value,并返回对应的下标r
,如果找不到,则-r-1
为应该插入的位置 -
fill(arr,value)
:将数组全部用value
填充 -
equals(arr1,arr2)
:判断两个数组是否相等(长度,对应的元素都是相等的才返回true)
多维数组
java多维数组可以是不规则的,
public class Main {
public static void main(String[] args) {
int[][] a = {{1,2,3},{2,3}};
int[][] b = new int[2][3];
System.out.println(a[1][1]);
System.out.println(Arrays.deepToString(a)); //toString无法处理多维数组
}
}
对象和类
本章只记录和C++的差别,类似的不记录。
构造器
默认字段初始化
如果没有显式地为字段设置处置,那么会被赋为默认值,数值为0,布尔为false
, 对象引用为null
无参构造器
如果类没有构造器,则会提供一个无参构造器,但如果提供了有参构造器,而没有提供无参构造器,则构造对象时不提供参数是错误的。
类的导入
自定义类的导入
和使用完全限定名相比,简单的方式是采用import 导入
静态导入
import static java.lang.Math.sqrt;
public class Main {
public static void main(String[] args) {
int a = 9;
int b = (int) sqrt(a);
System.out.println(b);
}
}
包访问权限
public:
可以被任意类使用private:
只能由定义它们的类使用- 没有指定
private
或者public
: 可以被同包的所有方法使用 protected
: 对本包和所有子类可见
和C++相比,protected
的保护的安全性变差了
继承
类,父类,子类
定义子类
java中用关键字extends
表示继承,且java中只有公有继承,同样可以重写父类的方法,如下面的run
方法,但如果还是想调用父类的,子类的run
方法中用关键字super
实现:super.run();
public class Student extends Person {
@Override
public void run() {
System.out.println("a student is running!");
}
public void eat(){
System.out.println("eating...");
}
}
子类的构造器
相同的字段,可以借助父类的构造器,依旧是利用super
调用构造器,这一句必须是子类构造器中的第一句。
int score;
public Student(String name,int age,int score){
super(name,age);
this.score = score;
}
java中动态绑定是默认的,在运行时自动选择恰当的方法,若不想一个方法是虚拟的,则用final
修饰它。
阻止继承:final类和方法
如Person
类加上了final
关键字,则Student
无法继承它。
public final class Person
同样给Person
类中的方法run
加上final
修饰,在子类中同样不能重写覆盖
public final void run(){
System.out.println("running!");
}
强制类型转换
java中子类强制转为父类是合法的,但反过来一般会报错,因此转换前用instanceof
检查。
if(p instanceof Student)
抽象类
抽象类不能实例化,将一个类声明为abstract
,这个类就成为了抽象类。用abstract
修饰的方法不用实现,类似C++的虚基类。
public abstract class Person {
String name;
int age;
public Person(String name,int age){
this.name = name;
this.age = age;
}
public abstract void run();
}
Object:所有类的父类
Object类型的变量
Object
类是所有类的超类(父类),但定义的时候不需要加上extends
。可用Object类引用任意类型的对象,当然要进行具体操作,还需要墙砖为对应的类型。对象数组和基本的数组都拓展了Object
类
Object obj = new Student("Alice", 23,89); //ok
Student st = (Student)obj;
Animal a = new Aninal[5];
obj = a; //0k
obj = new int[10]; //ok
equals方法
用于检测两个对象是否相等。
- 若引用同一个对象,则想等
- 另一个对象为
null
或者不是同一个类型,不相等 - 接下来判断各种字段,由于自己的
name
字段可能也是null
,故name.equals(o.name)
是有问题的,而应该用Objects.equals
方法 - 比较。
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
访问权限
接口,lambda表达式与内部类
接口
使用
概念:不是类,而是对符合这个接口的类的一组规范(听了想打人?),可以将接口看做没有实例字段的抽象类。接口中的方法都自动是public
.下面展示的是Arrays.sort承诺对对象数组进行排序,但需要对象所属的类实现下面的接口:
public interface Comparable{
in compareTo(Object other);
}
接口中可以有常量,或者抽象方法,由于接口规范默认是公开的,所以接口,接口内的方法的public
可以省略。
- 常量:
public,static,final
可以省略 - 方法:
public,abstract
可以省略
public interface InterfaceDemo {
String SITE_NAME = "shanghai";
void eat();
}
为何引入接口,而不是用抽象类呢?因为java
中类只能单继承,即只能拓展一个类,而接口提供了多重继承。
class Student extends Person, Comparable{...} //error
class Student extends Person implements Comparable{...} // ok
接口属性
接口不是类,故不能用new初始化一个接口,但可以声明接口的变量
package Test;
public interface Person {
default String getName(){return "person";}
}
package Test;
public interface Staff {
default String getName(){return getClass().getName()+"_"+hashCode();}
}
package Test;
public class Student implements Person, Staff{
@Override
public String getName() {
return Staff.super.getName();
}
}
package Test;
public class Main {
public static void main(String[] args) {
Person x;
x = new Student();
System.out.println(st.getName());
}
}
//
Test.Student_1854731462
接口默认方法冲突
两个接口中具有一个同名方法,且一个类继承了这两个接口,因此必须在Student
类中重新定义一个getName
方法覆盖两个接口的类,另外无论定义的接口变量是啥,引用了接口的类对象,调用的同名函数实际上还是类重新定义的函数。
package Test;
public class Main {
public static void main(String[] args) {
Student st = new Student();
System.out.println(st.getName());
}
}
Comparator接口
String
类实现了Comparable<String>
,且String.compareTo
方法默认按字典序排序,如果想把String
按长度排序又当如何呢?因为Arrays.sort提供了第二个版本,即数组+比较器,这个比较器可以自定义,为实现了Comparator
接口的类的实例。
package Test;
import java.util.Comparator;
public class MyCompare implements Comparator<String> {
@Override
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
}
package Test;
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
String[] s = {"abcd","xyz"};
Arrays.sort(s);
for(String str:s){
System.out.print(str +" ");
}
System.out.println();
Arrays.sort(s,new MyCompare());
for(String str:s){
System.out.print(str + " ");
}
}
}
//
abcd xyz
xyz abcd
对象克隆
通过=
赋值对象变量,实际上引用的是同一个对象,任何一个变量改变都会影响另一个。
var original = new Student();
Student cy = original;
cy.setName("Alice"); //also change the original
如果想复制得到一个新对象,初始状态和original相同,但对各自的操作是独立的,那么就该用clone方法
Student cy = original.clone();
cy.setName("Mike"); // original unchanged
但问题没那么简单,clone
方法是Object
的一个protected
方法,回顾以下,protected
方法只能在本包或者子类中可见,这个子类值得的在子类的方法中调用,而不能通过子类的实例调用。此外object类并不知道子类有哪些字段,只能逐字段拷贝,如果子类有其他对象的引用,拷贝的字段是另一个对象的引用,依旧会和其他对象共享信息,即为浅拷贝。解决的方法就是自定义一个clone
方法。对于一个类,需确定:
- 默认的
clone
方法是否满足要求 - 可否在可变的子对象上调用
clone
来修改默认的clone
方法 - 是否不该用
clone
若需要用clone
,应该实现Cloneable
接口,重新定义clone
方法,并用public
修饰。若默认的clone方法满足,还需重新实现Cloneable
接口,看下面的例子,看起来没做什么工作,实际上只是把它变为public,对外部可见,但若深拷贝需要做更多的工作。
public class Student implements Cloneable {
public Student clone() throws CloneNotSupportedException {
return (Student) super.clone();
}
}
lambda表达式
表达式语法
lambda
表达式为匿名函数,先看第一个表达式:(String s1, String s2)->s2.length() - s1.length()
,按字符串的长度逆序排序
package Test;
import java.util.Arrays;
import java.util.Comparator;
public class Main {
public static void main(String[] args) {
String[] s = {"abcd","xyz","world"};
Arrays.sort(s,(String s1, String s2)->s2.length() - s1.length());
for(String str:s){
System.out.print(str + " ");
}
}
}
//
world abcd xyz
另外需要注意,lambda表达式不允许满足特定条件的分支参有返回值,所有分支必须都有返回值。
函数式接口
只有一个抽象方法的接口,需要该接口的对象时,那么就lambda
表达式替代它
方法引用
若lambda表达式涉及一个方法,可以调用:
package Test;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class Main {
public static void main(String[] args) {
JFrame win = new JFrame("登录界面");
JButton btn = new JButton("点击");
btn.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("有人点了一下");
}
});
win.add(btn);
win.setVisible(true);
win.setSize(400,300);
}
}
将其修改为:
btn.addActionListener((ActionEvent e)-> {
System.out.println("有人点了一下");
});
简化规则
如上面的Arrays.sort(s,(String s1, String s2)->s2.length() - s1.length());
可进一步简化为:
Arrays.sort(s,(s1, s2)->s2.length() - s1.length());
处理lambda表达式
该表达式的重点是延迟执行,原因:
- 在单独线程执行代码
- 多次执行某段逻辑
- 在合适位置执行代码
- 某种情况下才执行
- 必要时才执行
若想某个动作执行n次,可将这个动作和重复次数传递给repeat方法:
package Test;
public class Main {
public static void repeat(int n, Runnable action){
while(n-- > 0){
action.run();
}
}
public static void main(String[] args) {
repeat(5, ()->System.out.println("hello,world"));
}
}
其他用法见Java核心技术 卷I p253
再谈Comparator
Comparator接口提供了很多方便的静态方法创建比较器,如将字符串按长度升序排序:
Arrays.sort(s, Comparator.comparingInt(String::length));
再如Person对象数组,按照名字对对象排序:
Arrays.sort(s, Comparator.comparing(Person::getName));
人名若相同,可以按年龄排序:
Arrays.sort(s, Comparator.comparing(Person::getName), (p1,p2)->Integer.compare(p1.getAge(),p2.getAge()));
内部类
使用场景、作用
- 事务内部需要一个完整的结构进行描述,如
People
类中有一个Heart
类 - 内部类可更方便访问外部类的数据,包括私有成员
- 提供更好的封装性,内部类可用
private
,protected
修饰。
静态内部类
加了static
修饰,和普通类没啥区别,使用:
Outer.Inner in = new Outer.Inner(); //先创建内部类
静态内部类可直接访问外部类的静态成员,但不可直接访问其他类型的成员。
package InnerClass_static;
public class Outer {
private static int score;
public static class Inner{
private String name;
private int age;
public Inner(String name,int age){
this.name = name;
this.age = age;
}
public void show(){
System.out.println(name + age + score);
}
}
}
成员内部类
无static
修饰,属于外部类的成员,JDK16开始,成员内部类支持静态成员,外部类成员访问没有限制范文。
Outer.Inner in = new Outer().new Inner();
访问所在外部类成员
class People{
private int heartBeat = 150;
public class Heart{
private int heartBeat = 110;
public void show(){
int heartBeat = 78;
System.out.println(heartBeat); // 78
System.out.println(this.heartBeat); // 110
System.out.println(People.this.heartBeat); // 150
}
}
}
匿名内部类
作用:方便创建子类对象,简化代码
定义:new 类名/抽象类名/接口名 { 方法 }
Heart h = new Heart(){
public void run(){
//do something
}
}
使用形式
package InnerClass;
public class Test {
public static void main(String[] args) {
/* Animal a = new Tiger();
*/
Animal a = new Animal() {
@Override
public void run() {
System.out.println("running fast");
}
};
a.run();
}
}
abstract class Animal{
public abstract void run();
}
/*
class Tiger extends Animal{
@Override
public void run() {
System.out.println("I'm running");
}
}*/
集合
概述
Collection接口
方法名称 | 说明 |
---|---|
boolean add(E e) | 添加元素,若成功,返回true |
boolean addAll(Collection c) | 添加集合c内所有元素 |
void clear() | 清空元素 |
boolean contains(Object o) | 检测是否含有指定元素 |
boolean containsAll(Collection c) | 检测是否含有集合c中所有元素 |
boolean isEmpty() | 检测集合是否为空 |
Iterator iterator() | 返回迭代器 |
boolean remove(Object o) | 删除第一个符合的指定元素 |
boolean removeAll(Collection c) | 删除所有在集合c中出现的元素,调用的集合改变了则返回true |
int size() | 返回集合元素个数 |
Object[] toArray() | 集合转为数组 |
迭代器
包含的方法:
public interface Iterator<E> {
boolean hasNext();
E next();
void remove();
default void forEachRemaining();
}
- hasNext:检测是否还含有元素
- next:下一个元素的迭代器
- remove:删除上次调用next方法时返回的元素。必须通过调用next才能调用remove
Iterator<String> it = list1.iterator();
it.next();
it.remove();
ArrayList
底层数据结构为数组,因此查询快,效率高,但线程不安全
构造方法及接口
ArrayList<E>() ; //构造一个容量为10的空数组
ArrayList<E>(int initialCapacity); //用指定容量构造空数组列表
public ArrayList(Collection<? extends E> c); //以集合c构造
- boolean add(E obj):在数组尾部增加元素obj,返回true
- int size(); 返回元素个数
- void ensureCapacity(int capasity); 和C++的vector的reserve()效果类似,预备capasity个对象的容量
- void trimToSize(): 将数组列表的存储容量缩减为当前大小
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<String > list1 = new ArrayList(); // 创建集合 list1
list1.add("one"); // 向 list1 添加一个元素
list1.add("two"); // 向 list1 添加一个元素
for(String s:list1){
System.out.println(s);
}
}
}
访问元素
ArrayList
不能使用[ ]访问指定索引的元素,而使用get
和set
方法。
- get(int index):返回索引为index处的元素
- set(int index, E obj): 将索引为index处的值设置为obj,并返回index位置的旧元素
链表LinkedList
java中链表都是双向的,每个节点都存放了前驱和后继的引用。构造:
LinkedList<E>();
LinkedList<Collection<? extends E> c);
常用API
- void addFirst(E e): 在链表头部增加元素
- void addLast(E e): 在尾部增加元素
- E removeFist():删除并返回头节点
- E removeLast():删除并返回尾节点
- E getFirst():获取链表头节点
- E getLast():获取链表尾节点
ListIterator接口
ListIterator继承自Iterator, 包含方法:
- void add(E e):增加元素,它假定该操作必定会改变链表
- E previous( ) :和next相反,迭代器往前一位,并返回该处的值
- boolean hasPrevious(): 检测前面是否有元素
- void set(E e):用新元素替换next或者previous访问的上一个元素
尽管java提供了访问某个特定位置元素的方法get(int index),但效率低(你懂的)。
package Test;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.ListIterator;
public class Main {
public static void main(String[] args) {
LinkedList<Integer> list = new LinkedList<>();
list.add(1);
list.add(3);
list.add(5);
ListIterator<Integer> iter = list.listIterator();
while (iter.hasNext()) {
System.out.print(iter.next() + " ");
}
System.out.println();
while(iter.hasPrevious()){
System.out.print(iter.previous() + " ");
}
System.out.println();
System.out.println(list.get(1));
}
}
//
1 3 5
5 3 1
3
HashSet
构造
HashSet();
Hashset(Collection<?extends E> elements);
HashSert(int cap);
HashSet(int cap, float loadFactor); //构造一个指定容量,装填因子的空散列表(当大约这个因子,就再散列)
例子
package Test;
import java.util.HashSet;
public class Main {
public static void main(String[] args) {
HashSet<Integer> s = new HashSet<>();
s.add(1);
s.add(3);
s.add(1);
System.out.println(s);
}
}
TreeSet
元素会自动排序,采用红黑树实现。
构造
TreeSet();
TreeSet(Comparator<?super E>comparator;
TreeSet(collection<? extends E> elements);
TreeSet(SortSet<E>s); //以已有的集合/有序集合构造
TreeSet继承了NavigableSet接口,对应的接口:
-
E higher/lower(E val):
返回排序后第一个大于/小于val的元素 -
E ceiling/floor(E val):
返回排序后第一个大于等于/小于等于val的元素 -
E pollFirst()/pollLast()
:删除并返回第一个/最后一个元素
队列和双端队列
Queue
boolean offer(E e);
//添加元素,如果队列已经满了,就返回false
E poll(); //队列为空,返回null
E peek(); //返回队首元素不删除,若为空,返回null
Deque
boolean offerFirst(E e); //队首增加元素e
boolean offerLast(E e); //队尾增加元素e
E pollFirst();
E pollLast(); //删除队首/队尾元素
E peekFirst();
E peekLast(); //返回队首/队尾元素
例子
package Test;
import java.util.*;
public class Main {
public static void main(String[] args) {
Deque<Integer>deque = new LinkedList<>();
deque.offerLast(4);
deque.offerFirst(1);
System.out.println(deque);
}
}
优先队列PriorityQueue
构造时可以提供容量,容量达到了再添加元素会扩容,默认是小根堆
public class Main {
public static void main(String[] args) {
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.add(1);
pq.add(8);
pq.add(4);
pq.add(2);
while(!pq.isEmpty()){
System.out.println(pq.remove());
}
}
}
大根堆的定义:
PriorityQueue<Integer> pq = new PriorityQueue<>(11, new Comparator<Integer>() {
//大顶堆,容量为11
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1;
}
});
替换为lambda表达式:
PriorityQueue<Integer> pq = new PriorityQueue<>(11, (o1, o2) -> o2-o1);
映射HashMap,TreeMap
二者都实现了Map<K,V>
的接口,
V get(object K):
获取键对应的值,没有就返回nulldefault V getOrDefault(object key, V val)
:获取值,若没有这个键,则返回设定的默认值,注意 这个接口不会将键,值插入V put(K key, V value);
存入键值对,若键已存在,则返回旧值,反正返回nullvoid putAll(Map<? extends K, extends V>entries)
将entries的条目放入映射boolean containsKey(Object key);
是否含有键
LinkedHashSet/LinkHashMap
变化的是插入的各个元素之间组成了双向链表,记录了相对位置。此外LRU缓存有个最近最少使用,采用LinkHashMap也可以实现,最后一个参数设置为true即可。
Map<Integer,Integer> mp = new LinkedHashMap<>(12, 0.75F,true);
mp.put(5,2);
mp.put(10,5);
mp.put(4,5);
mp.put(1,5);
System.out.println(mp);
mp.get(5);
System.out.println(mp);
/*
{5=2, 10=5, 4=5, 1=5}
{10=5, 4=5, 1=5, 5=2}
*/
多线程
创建
创建有三种方式
继承自Thread
最后创建方法,Test t = new Test(); t.start();
package MyThread;
public class Test extends Thread{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程开始:"+i);
}
}
}
缺点:继承的Thread类功能比较单一。注意:直接用run方法,会被当做普通方法,而不能实现多线程。
继承Runable接口
- 定义线程任务类
MyRunnable
,重写run
方法 - 创建
MyRunnable
任务对象 - 把
MyRunnable
对象交给Thead处理 - 调用
start()
处理
package MyThread;
public class Test implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("子线程开始**********:"+i);
}
}
}
Test th = new Test();
Thread t = new Thread(th);
t.start();
可以采用匿名的方法实现。优点:扩展性更强。缺点:多一层封装,无法处理有返回值的线程任务
实现Callable接口
- 得到任务对象:1. 定义类实现Callable接口,重写call方法,封装要做的事; 2. 用FutureTask把Callable对象封装成线程对象。
- 把FutureTask传递给Thread创建对象
package MyThread;
import java.util.concurrent.Callable;
public class Test implements Callable<Integer> {
private int n;
public Test(int n){
this.n = n;
}
@Override
public Integer call() throws Exception {
int sum = 0;
for(int i = 1; i <= n;++i){
sum += i;
}
return sum;
}
}
package MyThread;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Test th = new Test(100);
FutureTask<Integer> ft = new FutureTask<>(th);
Thread t1 = new Thread(ft);
t1.start();
System.out.println(ft.get()); //等待线程结束才提取结果
}
}
//5050
优点:扩展性强,可获得线程执行的结果。缺点:编程相对复杂
Thread常用方法
String getName()
:获取线程名字
但也可以直接起名字,实际开发中并不自己起名字,因为默认就有名字
package MyThread;
public class Test extends Thread{
Test(){
}
Test(String name){
super(name);
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
package MyThread;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread t1 = new Test("1号");
//t1.setName("1号");
t1.start();
Thread t2 = new Test("2号");
// t2.setName("2号");
t2.start();
Thread cur = Thread.currentThread();
System.out.println(cur.getName()); //等待线程结束才提取结果
for (int i = 0; i < 5; i++) {
System.out.println("main线程:"+i);
}
}
}
public static void sleep(long time)
:线程睡眠time毫秒
package MyThread;
import java.util.concurrent.ExecutionException;
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
for (int i = 0; i < 5; i++) {
if(i == 3){
Thread.sleep(3000); //本线程这里休眠3000毫秒
}
System.out.println("i:"+i);
}
}
}
同步机制
以双方从一个共享账户取钱为例
账户类
package MyThread;
public class Account {
private String cardId;
private double money;
public Account(String cardId, double money) {
this.cardId = cardId;
this.money = money;
}
public void withdraw(String name, int money) {
Thread t = Thread.currentThread();
synchronized (this){
if(this.money >= money){
System.out.println(name + "取走" + money + "元");
this.money -= money;
System.out.println("账户余额:" + this.money);
}
else{
System.out.println(name + "的余额为:" + this.money + ",取钱失败!");
}
}
}
}
用户类
package MyThread;
public class UserThread extends Thread{
private String name;
private int money;
private Account account;
UserThread(String name, int money, Account account){
this.name = name;
this.money = money;
this.account = account;
}
@Override
public void run() {
account.withdraw(name, money);
}
}
main函数调用
package MyThread;
public class Main {
public static void main(String[] args) {
Account account = new Account("M201972608",100);
UserThread user1 = new UserThread("张三",100,account);
user1.start();
UserThread user2 = new UserThread("李四",100,account);
user2.start();
}
}
如果没有synchronized
设置同步,执行结果为最后账户余额:-100.0,,而采用synchronized
后,逻辑正确。synchronized
提倡用唯一的不变独享对象作为锁,但不可用全局唯一的,因此面向对象实现时,提倡用this。如果静态方法,同步默认用类名.class
.此外也可以把方法设置为同步方法public synchronized void withdraw(String name, int money)
,这个效率虽然低一点,但是写法简单,根据方法名就知道要完成同步。
Lock锁
- 定义锁:
private final Lock lock = new ReentrantLock();
- 上锁:
lock.lock()
- 解锁:
lock.unlock()
此外由于开发中安全起见,在try-final中上锁解锁,即及时中间代码执行出异常,也能成功解锁。在之前的withdraw
方法中上锁,在try里面故意设置一个异常,finally里面解锁
public void withdraw(String name, int money) {
Thread t = Thread.currentThread();
lock.lock();
try {
if(this.money >= money){
System.out.println(name + "取走" + money + "元");
this.money -= money;
System.out.println("账户余额:" + this.money);
}
else{
System.out.println(name + "的余额为:" + this.money + ",取钱失败!");
}
System.out.println(10 / 0);
}
finally {
lock.unlock();
System.out.println("解锁啦!");
}
}
发现即使出现了异常,锁依旧被解开(防止开发过程中,某一处出现错误占用锁被释放,造成死锁。
线程间通信
以生产者消费者线程为例:
共享资源类
package Produce_Consumer;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Goods {
private int num_goods = 0;
private final Lock lock = new ReentrantLock();
private static final int Capacity = 1000;
public synchronized void produce(int add){
try{
if(num_goods < Capacity ){
System.out.println("生产者进入,当前产品数为:"+this.num_goods);
num_goods += add;
System.out.println(Thread.currentThread().getName() + "生产了1个产品,当前产品数为:"+this.num_goods);
}
this.notifyAll();
this.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
public synchronized void consume(int sub){
try{
if(num_goods >= sub){
System.out.println("消费者进入,当前产品数为:"+this.num_goods);
num_goods -= sub;
System.out.println(Thread.currentThread().getName() + "消费了1个产品,当前产品数为:"+this.num_goods);
}
this.notifyAll();
this.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
}
生产者线程
package Produce_Consumer;
public class Producer extends Thread{
private Goods goods;
Producer(Goods goods){
this.goods = goods;
}
@Override
public void run() {
while(true){
try {
Thread.sleep(1000);
goods.produce(100);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
消费者线程
package Produce_Consumer;
public class Consumer extends Thread {
private Goods goods;
Consumer(Goods goods){
this.goods = goods;
}
@Override
public void run() {
while(true){
try {
Thread.sleep(2000);
goods.consume(500);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
Main调用
package Produce_Consumer;
public class Main {
public static void main(String[] args) {
Goods goods = new Goods();
new Producer(goods).start();
new Producer(goods).start();
new Producer(goods).start();
new Consumer(goods).start();
new Consumer(goods).start();
new Consumer(goods).start();
new Consumer(goods).start();
}
}
线程池
优点:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
java的线程池的实现类ThreadPoolExecutor
,参数最全的构造器:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
核心线程始终存活,临时线程可能用完就销毁。新任务来了,发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,就会创建它。
创建任务
package ThreadPool;
public class MyRunnable implements Runnable {
@Override
public void run() {
for(int i = 0;i < 5;++i){
System.out.println(Thread.currentThread().getName() + " is printing " + i);
try{
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
创建线程池,添加任务到线程池
package ThreadPool;
import java.util.concurrent.*;
public class ThreadPoolTest {
public static void main(String[] args) {
ExecutorService pool = new ThreadPoolExecutor(3,5,6, TimeUnit.SECONDS,new ArrayBlockingQueue<>(5),
Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
Runnable target = new MyRunnable();
int n = 100;
while(--n > 0){
pool.execute(target);
}
}
}
关闭线程池: pool.shutdown()
或者 pool.shutdownNow()
,前者将正在执行的任何执行完再关闭,后者立即关闭
利用线程池提交有返回值的任务
任务
package ThreadPool;
import java.util.concurrent.Callable;
public class MyCallable implements Callable<String> {
private int n;
MyCallable(int n){
this.n = n;
}
@Override
public String call() throws Exception {
int sum = 0;
for(int i = 1; i <= n;++i){
sum += i;
}
return Thread.currentThread().getName() + "计算1+2+...+" + n +"的和,结果为:" + sum;
}
}
执行任务
package ThreadPool;
import java.util.concurrent.*;
public class ThreadPoolTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService pool = new ThreadPoolExecutor(3,5,6, TimeUnit.SECONDS,new ArrayBlockingQueue<>(5),
Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
Future<String> f1 = pool.submit(new MyCallable(100));
Future<String> f2 = pool.submit(new MyCallable(100));
Future<String> f3 = pool.submit(new MyCallable(100));
Future<String> f4 = pool.submit(new MyCallable(100));
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
System.out.println(f4.get());
pool.shutdown(); //不加这个主线程不会结束
}
}
Executors得到线程池对象的常用方法
定时器
控制任务延迟调用或者周期调用。如视频定时加载广告,闹钟,邮件定时发送。
Timer定时器
Timer t = new Timer();
t.schedule(new TimerTask() {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "访问了AAA!");
}
},2000,1000);
在当前时间,延迟2000毫秒后,没间隔1000毫秒执行任务。Timer定时器虽然简单,但是缺点明显:收到该定时器其他任务的干扰,如下面的例子,第一个任务中延迟了5000毫秒,结果第二个任务是才7秒才执行,且某个任务挂了,其他任务也挂
package Main;
import java.util.Timer;
import java.util.TimerTask;
public class Main {
public static void main(String[] args) {
Timer t = new Timer();
t.schedule(new TimerTask() {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "访问了AAA!");
}
},2000,1000);
t.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "访问了BBB!");
}
},2000,1000);
}
}
ScheduledExecutorService线程池制作定时器
package Main;
import java.sql.Time;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class Main {
public static void main(String[] args) {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);
pool.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is printing AAA.");
}
},5,2, TimeUnit.SECONDS);
pool.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is printing BBB.");
}
},0,2, TimeUnit.SECONDS);
}
}
延迟$x$秒后,每次间隔$y$秒执行run内的任务。优点:采用了多线程技术,互不干扰(某一个挂了,其他不受影响)
阻塞队列
ArrayBlockingQueue
底层通过数组实现,必须指定容量。接口:
put:
队尾存放元素,若队列已经满了再次存放会阻塞(锁实现)take:
队首取出元素,若队列为空再取元素会阻塞(锁实现)
用同一把锁同步存和取操作,是因为二者是会相互影响的过程:通过源码发现ArrayBlockingQueue
维护了一个int变量count记录当前队列的元素个数,而存、取都会修改这个变量,如果不用同一把锁,会造成这个变量修改出现问题。而LinkBlockingQueue
的元素存取各自采用了一把锁,但是用的是原子变量AtomicInteger
记录元素个数。同时存放没有问题,多个同时修改原子变量,只有一个线程能成功。
网络编程
TCP通信
单线程多发多收
客户端
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class Client {
public static void main(String[] args) {
try{
Socket socket = new Socket("127.0.0.1",7777);
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(),"utf-8"));
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream(), "utf-8"));
bw.write("你好,服务器");
bw.newLine();
bw.flush();
String data = br.readLine();
System.out.println(socket.getRemoteSocketAddress()+ "发送了:" + data);
Scanner sc = new Scanner(System.in);
while(true) {
String msg = sc.nextLine();
bw.write(msg);
bw.newLine();
bw.flush();
}
// socket.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
服务器
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) {
try{
ServerSocket serversocket = new ServerSocket(7777);
Socket socket = serversocket.accept();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(),"utf-8"));
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream(), "utf-8"));
bw.write("你好,客户端!");
bw.newLine();
bw.flush();
String msg;
while((msg = br.readLine()) != null){
System.out.println(socket.getRemoteSocketAddress() + "发送了:" + msg);
}
}
catch (Exception e){
e.printStackTrace();
}
}
}
需要注意的是,发送(写到缓存)消息时,每次发送写入后需要发送一个换行符,不然会卡主。然而,服务器无法接收多个客户端的消息。
服务器多线程
客户端不变,修改服务器端。
- 主线程:循环接收socket
- 子线程:处理单个socket的消息
服务器
import java.net.ServerSocket;
import java.net.Socket;
public class MultiServer {
public static void main(String[] args) {
try{
System.out.println("服务器启动!");
ServerSocket server = new ServerSocket(7777);
while(true){
Socket socket = server.accept();
new ServerReaderThread(socket).start();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
处理消息的子线程
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerReaderThread extends Thread {
private Socket socket;
public ServerReaderThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream(),"utf-8"));
String msg;
while((msg = br.readLine()) != null){
System.out.println(socket.getRemoteSocketAddress() +" send:" + msg);
}
} catch (IOException e) {
System.out.println(socket.getRemoteSocketAddress() +"下线了");
// throw new RuntimeException(e);
}
}
}
以上实现看似解决了问题,但是高并发场景,大量客户端请求过来,那么会创建大量线程处理对应的请求,所以需要利用线程池优化
线程池优化
修改服务器,采用线程池优化。优点:复用线程处理多个客户端,适用于客户端连接时间较短的场景。
处理任务的接口
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerReaderRunnable implements Runnable {
private Socket socket;
public ServerReaderRunnable(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream(),"utf-8"));
String msg;
while((msg = br.readLine()) != null){
System.out.println(socket.getRemoteSocketAddress() +" send:" + msg);
}
} catch (IOException e) {
System.out.println(socket.getRemoteSocketAddress() +"下线了");
// throw new RuntimeException(e);
}
}
}
服务器端
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.*;
public class MultiServer {
private static ExecutorService pool = new ThreadPoolExecutor(3,5,6, TimeUnit.SECONDS,new ArrayBlockingQueue<>(2),
Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) {
try{
System.out.println("服务器启动!");
ServerSocket server = new ServerSocket(7777);
while(true){
Socket socket = server.accept();
Runnable target = new ServerReaderRunnable(socket);
pool.execute(target);
}
}catch (Exception e){
e.printStackTrace();
}
}
}
群聊模式
服务器群发从客户端接收的消息。所以客户端具备接收和发送消息的能力
package PortForward;
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
/*
客户端同时处理接收和发送
*/
public class Client {
public static void main(String[] args) {
try{
Socket socket = new Socket("127.0.0.1",7777);
new ClientReadThread(socket).start();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(),"utf-8"));
WriteMsg(bw);
}catch(Exception e){
e.printStackTrace();
}
}
public static void WriteMsg(BufferedWriter bw) throws IOException {
bw.write("你好,服务器");
bw.newLine();
bw.flush();
Scanner sc = new Scanner(System.in);
while(true) {
String msg = sc.nextLine();
bw.write(msg);
bw.newLine();
bw.flush();
}
}
}
package PortForward;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
public class ClientReadThread extends Thread{
private Socket socket;
public ClientReadThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream(),"utf-8"));
String msg;
while((msg = br.readLine()) != null){
System.out.println(socket.getRemoteSocketAddress() +" send:" + msg);
}
} catch (IOException e) {
System.out.println(socket.getRemoteSocketAddress() +"把你踢出去了!");
}
}
}
服务器端要群发的话,需要用一个静态List记录连接的socket,然后将从某个客户端接收的消息发送给其他客户端。
package PortForward;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
public class Server {
public static List<Socket> allOnLineSocket = new ArrayList<>();
public static void main(String[] args) {
try {
System.out.println("服务器启动!");
ServerSocket server = new ServerSocket(7777);
while (true) {
Socket socket = server.accept();
allOnLineSocket.add(socket);
new ServerReaderThread(socket).start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
package PortForward;
import java.io.*;
import java.net.Socket;
public class ServerReaderThread extends Thread {
private Socket socket;
public ServerReaderThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream(),"utf-8"));
String msg;
while((msg = br.readLine()) != null){
System.out.println(socket.getRemoteSocketAddress() +" send:" + msg);
sendMsgToAll(msg,socket);
}
} catch (IOException e) {
System.out.println(socket.getRemoteSocketAddress() +"下线了");
Server.allOnLineSocket.remove(socket);
// throw new RuntimeException(e);
}
}
private void sendMsgToAll(String msg,Socket mysocket) throws IOException {
for(Socket socket: Server.allOnLineSocket){
if(socket == mysocket) continue;
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(),"utf-8"));
bw.write(msg);
bw.newLine();
bw.flush();
}
}
}
BS模型服务器
客户端为浏览器,因此需要按照网络协议发送给浏览器信息
package BSserver;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) {
try{
ServerSocket server = new ServerSocket(8080);
while (true) {
Socket socket = server.accept();
new ServerReaderThread(socket).start();
}
}
catch (Exception e){
e.printStackTrace();
}
}
}
package BSserver;
import java.io.*;
import java.net.Socket;
public class ServerReaderThread extends Thread {
private Socket socket;
public ServerReaderThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(),"utf-8"));
StringBuffer str = new StringBuffer();
str.append("HTTP/1.1 200 OK\r\n");
str.append("Content-type:text/html;charset=utf-8\r\n");
str.append("\r\n");
str.append("<html><head><title>网页</title></head><body>开眼看时间<body></html>");
bw.write(str.toString());
bw.flush();
bw.close();
/* PrintStream ps = new PrintStream(socket.getOutputStream());
ps.println("HTTP/1.1 200 OK");
ps.println("Content-Type:text/html;charset=gbk");
ps.println();
ps.println("<span style='color:red; font-size=500px'> 开眼看世界! </span>");
ps.close();*/
} catch (IOException e) {
e.printStackTrace();
}
}
}
异常
异常分类
异对对象都派生于Throwable
类的实例。下一层分为两个分支:
-
Error:内存错误和资源耗尽的错误,用户几乎无能为例
-
Exception:再分解为两个分支:一个派生于RuntimeException,编程错误导致的异常,另一个分支包含其他异常,如程序本身没问题,但是I/O错误导致的异常。
派生于RuntimeException的异常包括:
-
错误的类型转换
-
数组访问越界
-
访问null指针:
String name = null; System.out.println(name.length()); //nullPointerException
其他异常:
- 试图打开不存在的文件
- 试图越过文件尾继续读取数据
默认的异常处理
默认在异常的代码哪里创建一个异常对象,抛出给调用者,调用者最终抛出给JVM,最终程序直接挂了。缺点:异常导致系统全部瘫痪。
异常处理
三种方式:
- 层层上抛(依旧会导致程序死亡),规范上抛:
方法名 throw Exception
- 自己处理,通过try catch。出现了异常不会导致程序挂了。try包裹容易出现问题的程序,catch捕获异常处理
public class Main {
public static void main(String[] args) {
try {
int a = 2;
int b = 0;
System.out.println(a / b);
} catch (Exception e) {
System.out.println("出现了除以0的异常");
}
System.out.println("我运行到这了哦!");
}
}
//运行结果
出现了除以0的异常
我运行到这了哦!
- 抛给上层调用者,由上层调用者决定怎么处理。
public class Main {
public static void main(String[] args) {
try {
test();
} catch (Exception e) {
System.out.println("下层抛给我的异常");
}
System.out.println("我运行到这了哦!");
}
public static void test() throws Exception{
System.out.println(3 / 0);
}
}
//
下层抛给我的异常
我运行到这了哦!
异常处理
编译时异常处理形式有三种:
- 层层上抛(依旧抛给虚拟机,程序死亡),规范做法:
方法名 throw Exception
- 自己捕获异常处理:
try{
容易出问题的代码
}catch(异常变量 变量1){
}
catch(异常变量 变量2){
}
...
- 抛出给调用者,调用者处理:这种处理是比较好的
日志
日志相对于直接打印的优点:
- 存储于文件或者数据库
- 随时以开关控制是否记录日志,无需改代码
- 多线程场景性能好
Logback
是由log4j创始人设计一款性能更好开源日志
使用logback
安装与配置
先安装以下三个文件:logback下载地址,slf4j下载地址
- slf4j-api-1.27x.jar
- logback-classic-1.2.x.jar
- logback-core-1.2.x.jar
创建配置文件logback.xml到src目录下,文件内容如下
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>utf-8</charset>
</encoder>
<file>log/output.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<fileNamePattern>log/output.log.%i</fileNamePattern>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>1MB</MaxFileSize>
</triggeringPolicy>
</appender>
<root level="all">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
</configuration>
接着将上面在项目下创建文件夹lib,把上面三个jar文件复制到里面,鼠标右键点击:添加为库(项目库)
测试
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Main {
public static final Logger LOGGER = LoggerFactory.getLogger("Main.class");
public static void main(String[] args) {
LOGGER.debug("调试开始");
LOGGER.info("我要开始做除法");
try {
int a = 1;
int b = 0;
LOGGER.trace("a = " + a);
LOGGER.trace("b = " + b);
System.out.println(a / b);
} catch (Exception e) {
e.printStackTrace();
LOGGER.error("异常出现了" + e);
}
}
}
配置文件里的level=all表示所有级别都显示,控制台最终显示:
此外所有历史记录都在该项目的log文件夹下的output.log里面(自动创建)
日志级别
级别层次:error > warn > info > debug > trace。例如在logback中配置显示的日志信息为info,则只有级别不低于info的才显示。此外,设置all/off,表示显示/关闭所有日志信息
反射
获取类
//静态方法forName
Class<?> c1 = Class.forName("JavaBean.Person"); // class JavaBean.Person
//类名
Class c2 = Person.class;
//对象.class
Person p = new Person();
Class<? extends Person> c3 = p.getClass(); // class JavaBean.Person
获取构造器
Constructor<?>[] constructors = c1.getConstructors(); //只能获取公开的构造器
Constructor<?>[] constructors = c1.getDeclaredConstructors(); //公开,私有都获取
for( Constructor<?> constructor:constructors){
System.out.println(constructor);
}
文件
try-with-resource
概念
用try语句声明资源, 资源对象在执行完自动关闭. 任何实现了java.lang.AutoCloseable
包括java.io.Closeable
的对象,采用这种写法都能自动关闭资源, 即使出现异常.
public class MyAutoClosable implements AutoCloseable {
public void doInit() {
System.out.println(1 / 0);
System.out.println("MyAutoClosable doing it!");
}
@Override
public void close() throws Exception {
System.out.println("MyAutoClosable closed!");
}
public static void main(String[] args) {
try(MyAutoClosable myAutoClosable = new MyAutoClosable()){
myAutoClosable.doInit();
} catch (Exception e) {
e.printStackTrace();
}
}
}
结果发现: 即使出现异常, 自动调用close方法.
使用场景4
读取文件过程中, 如果出现异常, 我们往往希望将文件关闭, 防止损坏文件. 传统处理方法:
public class Main {
public static void main(String[] args) throws FileNotFoundException {
Main obj = new Main();
obj.readFile();
}
public void readFile() {
FileReader fr = null;
BufferedReader br = null;
try{
fr = new FileReader("d:/1.txt");
br = new BufferedReader(fr);
System.out.println(1 / 0); // 人为制造异常
String s = "";
while((s = br.readLine()) != null){
System.out.println(s);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
System.out.println("开始关闭");
br.close();
fr.close();
System.out.println("关闭成功");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
执行结果:
换用try-with-resouce写法十分简洁, 且正确性能得到保证. 其中FileReader 和BufferedReader 都实现了Closeable的接口
public void readFileWithTryResource() throws FileNotFoundException {
try(
FileReader fr = new FileReader("d:/input.txt");
BufferedReader br = new BufferedReader(fr)
){
String s = "";
while((s = br.readLine()) != null){
System.out.println(s);
}
} catch (IOException e) {
e.printStackTrace();
}
}
泛型
何为泛型
泛型就是定义一种模板, 使用时实例化该类型. 我理解的是和C++的模板编程基本一样
ArrayList<String> strList = new ArrayList<String>();
List<Integer> integerList = new ArrayList<Integer>();
向上转型
可以转为接口类型, 如上面的List实际上为接口interface, 虽然Number是Integer的父类, 但是不能将 ArrayList<Integer>
其转为ArrayList<Number>
. 原因很简单, 转为ArrayList<Number>
后可以添加double, float等类型的数据, 但它本身是ArrayList<Integer>
,不能添加其他类型数据, 故编译器禁止了这种做法. 根本上: ArrayList<Number>
和ArrayList<Integer>
没有继承关系.
T类型和?通配符
表示括号里要用到泛型参数,
List<T> al = new ArrayList<T>();指定集合元素只能是T类型
List<?> al = new ArrayList<?>();集合元素可以是任意类型,这种没有意义,一般是方法中,只是为了说明用法
List<? extends E> al = new ArrayList<? extends E>(); E:接收E类型或者E的子类型
Integer[] arr = {1, 2, 3};
Double[] arr2 = {1.2d, 2.5d};
String[] arr3 = {"aa", "bb"};
ArrayList<Integer> arrayList1 = new ArrayList<>(Arrays.asList(arr));
ArrayList<Double> arrayList2 = new ArrayList<>(Arrays.asList(arr2));
ArrayList<String> arrayList3 = new ArrayList<>(Arrays.asList(arr3));
List<? extends Number> list = arrayList1;
System.out.println(x);
System.out.println(list);
list = arrayList2;
System.out.println(list);
// list = arrayList3; // error
- < ? extends E>: 限定了只能接受E类型或者E的子类型. 故上面list = arrayList3无法通过编译
- ?super E: 限定接收E类型或者E的父类型.
ArrrayList中< ? extends E>的一种典型应用: 其中E为初始限定ArrayList的模板类型, 可以保证传入数据类型的正确性.
//ArrayList集合的构造器,extends代表E范型或是E范型的子类,否则报错。
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}