1、异常
可检测异常:不可控错误,无法在逻辑层面解决所有问题;
非检测异常:逻辑错误Bug,可以通过代码完全避免;常见的有RuntimeException;
RuntimeException异常子类:
NullPointerException - 空指针引用异常
ClassCastException - 类型强制转换异常。
IllegalArgumentException - 传递非法参数异常。
ArithmeticException - 算术运算异常
ArrayStoreException - 向数组中存放与声明类型不兼容对象异常
IndexOutOfBoundsException - 下标越界异常
NegativeArraySizeException - 创建一个大小为负数的数组错误异常
NumberFormatException - 数字格式异常
SecurityException - 安全异常
UnsupportedOperationException - 不支持的操作异常
自定义异常:常用于满足语法但不满足需求时,自己去自定义一个新异常;要求异常类名需要做到见名知义;它继承自Exception(直接或间接继承);提供所有构造器(IDEA生成即可);
public class IllegalAgeException extends Exception{
public IllegalAgeException() {
}
public IllegalAgeException(String message) {
super(message);
}
public IllegalAgeException(String message, Throwable cause) {
super(message, cause);
}
public IllegalAgeException(Throwable cause) {
super(cause);
}
public IllegalAgeException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
抛出自定义异常:
public class Person {
private int age;
public int getAge() {
return age;
}
/*
针对一个单独的特类异常,声明里的throws异常最好也写成相应的异常
*/
public void setAge(int age) throws IllegalAgeException {
if(age<0||age>100){
throw new IllegalAgeException();
}
this.age = age;
}
}
2、网络编程基础
Client:客户端 Server:服务端
传输协议:
(TCP)可靠传输:效率低,需要反复确认;
(UDP)不可靠传输:效率高,但可能会丢包,网络程序主流协议;
java.net.Socket:Socket(套接字)封装了TCP协议的通讯细节,使用它可以与服务端建立网络链接,并获取两个流(一个输入一个输出),然后使用这两个流的读写操作完成与服务端的数据交互;
Socket提供了两个重要的方法:
OutputStream getOutputStream();
该方法会获取一个字节输出流,通过这个输出流写出的字节数据会通过网络发送给对方。
InputStream getInputStream();
通过该方法获取的字节输入流读取的是远端计算机发送过来的数据。
java.net.ServerSocket:运行在服务端,它的作用有两个:
1:向系统申请服务端口,客户端的Socket就是通过这个端口与服务端建立连接的。
2:监听服务端口,一旦一个客户端通过该端口建立连接则会自动创建一个Socket,并通过该Socket与客户端进行数据交互。
实例化ServerSocket时要指定服务端口,端口是一个数字,取值范围:0-65535之间;6000之前的的端口不要使用,密集绑定系统应用和流行应用程序;该端口不能与操作系统其它应用程序占用的端口相同,否则会抛出异常:java.net.BindException:address already in use
ServerSocket提供了接受客户端链接的方法:Socket accept()
这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端的链接,直到一个客户端链接,此时该方法会立即返回一个Socket实例,通过这个Socket就可以与客户端进行交互了;
先创建一个简易的网络聊天室:一个服务端和一个客户端,客户端可以链接服务端并发送消息;
服务端:
public class Server {
private ServerSocket serverSocket;
public Server(){
try {
System.out.println("正在启动服务端...");
serverSocket = new ServerSocket(8088);
System.out.println("服务端启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
public void start(){
try {
System.out.println("等待客户端链接...");
Socket socket = serverSocket.accept();
System.out.println("一个客户端链接了!");
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr);
String line;
/*
这里我们使用BufferedReader的readLine试图读取来自远端计算机
发送过来的一行字符串时,如果对方链接还保持着,但是没有发送任何
内容时,这里的readLine会阻塞。
客户端断开链接时,服务端的readLine方法可能出现以下几种情况:
最正常的情况:客户端调用了socket的close方法与服务端断开链接
socket的close方法会进行底层TCP的挥手动作与对方断开链接。
那么readLine方法这里会立即返回null,表示链接断开了。不能再读取数据了
客户端若异常断开链接,在没有调用socket.close()就结束了程序,那么
服务端这里的readLine()方法会抛出异常:
java.net.SocketException: Connection reset
*/
while((line = br.readLine())!=null) {
System.out.println("客户端说:" + line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
}
客户端:
public class Cli {
private Socket socket;
Scanner sc = new Scanner(System.in);
public static void main(String[] args) {
Cli cli = new Cli();
cli.run();
}
Cli(){
System.out.println("初始化客户端");
try {
socket = new Socket("localhost",8088);
System.out.println("客户端初始化成功");
} catch (IOException e) {
e.printStackTrace();
}
}
public void run(){
try {
System.out.println("请打字");
OutputStream os = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(os, StandardCharsets.UTF_8);
BufferedWriter bw = new BufferedWriter(osw);
PrintWriter pw = new PrintWriter(bw,true);
while(true){
String word = sc.nextLine();
if("exit".equals(word)){
break;
}
pw.println(word);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
3、并发
多客户端链接:之前由于服务端accept()只用了一次,只能链接一个客户端的socket;想要多客户端链接,需要循环调用accept()去接受客户端的socket;但是由于服务端里的readline()是一个循环,第一个客户端不下线,readline()永远都不能返回null,就永远都在这个循环里跳不出去;因此想要创建多客户端链接,我们需要用到多线程;
线程:一个顺序的单一的程序执行流程就是一个线程,代码一句一句的有先后顺序的执行;
多线程:多个单一顺序执行的流程并发运行。造成"感官上同时运行"的效果。
并发:多个线程实际运行是走走停停的。线程调度程序会将CPU运行时间划分为若干个时间片段并尽可能均匀的分配给每个线程,拿到时间片的线程被CPU执行这段时间。当超时后线程调度程序会再次分配一个时间片段给一个线程使得CPU执行它。如此反复。由于CPU执行时间在纳秒级别,我们感觉不到切换线程运行的过程。所以微观上走走停停,宏观上感觉一起运行的现象成为并发运行。
线程的生命周期图:
java里实现线程的执行单元有两种方法:
1、Thread方法
定义一个线程类,重写run方法,在其中定义线程要执行的任务(希望和其他线程并发执行的任务)。这种方法的优点是结构简单,利于匿名内部类的创建;
public class ThreadDemo1 {
public static void main(String[] args) {
Thread t1 = new MyThread1();
Thread t2 = new MyThread2();
t1.start();
t2.start();
}
}
class MyThread1 extends Thread{
public void run(){
for(int i=0;i<1000;i++){
System.out.println("你是谁啊?");
}
}
}
class MyThread2 extends Thread{
public void run(){
for(int i=0;i<1000;i++){
System.out.println("开门!查水表的!");
}
}
}
缺点是:
一、java是单继承,导致该线程类无法继承其他类;
二、定义线程的同时重写了run方法,会导致线程与线程任务绑定在了一起,不利于线程的重用;
注意:启动该线程要调用该线程的start方法,而不是run方法;
2、Runnerable方法
实现Runnable接口单独定义线程任务
public class ThreadDemo2 {
public static void main(String[] args) {
//创建线程任务
Runnable r1 = new MyRunnable1();
Runnable r2 = new MyRunnable2();
//创建两个线程
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
}
}
class MyRunnable1 implements Runnable{
public void run(){
for(int i=0;i<1000;i++){
System.out.println("你是谁啊?");
}
}
}
class MyRunnable2 implements Runnable{
public void run(){
for(int i=0;i<1000;i++){
System.out.println("我是查水表的!");
}
}
}
线程的一些API介绍:
public class ThreadInfoDemo {
public static void main(String[] args) {
Thread main = Thread.currentThread();//获取主线程
String name = main.getName();//获取线程的名字
System.out.println("名字:"+name);
long id = main.getId();//获取该线程的唯一标识
System.out.println("id:"+id);
int priority = main.getPriority();//获取该线程的优先级
System.out.println("优先级:"+priority);
boolean isAlive = main.isAlive();//该线程是否活着
System.out.println("是否活着:"+isAlive);
boolean isDaemon = main.isDaemon();//是否为守护线程
System.out.println("是否为守护线程:"+isDaemon);
boolean isInterrupted = main.isInterrupted();//是否被中断了
System.out.println("是否被中断了:"+isInterrupted);
}
}
线程名字:
java中的代码都是靠线程运行的,执行main方法的线程称为"主线程";我们自己定义的线程在不指定名字的情况下系统会分配一个名字,格式为"thread-x"(x是一个数)。
线程优先级:
线程start后会纳入到线程调度器中统一管理,线程只能被动的被分配时间片并发运行,而无法主动索取时间片.线程调度器尽可能均匀的将时间片分配给每个线程;
线程有10个优先级,使用整数1-10表示:1为最小优先级,10为最高优先级.5为默认值;调整线程的优先级可以最大程度的干涉获取时间片的几率.优先级越高的线程获取时间片的次数越多,反之则越少;
sleep阻塞:static void sleep(long ms)
- 使运行该方法的线程进入阻塞状态指定的毫秒,超时后线程会自动回到RUNNABLE状态等待再次获取时间片并发运行;sleep方法要求必须处理中断异常:InterruptedException;
* 当一个线程调用sleep方法处于睡眠阻塞的过程中,它的interrupt()方法被调用时会中断该阻塞,此时sleep方法会抛出该异常;
public class SleepDemo2 {
public static void main(String[] args) {
Thread lin = new Thread(){
public void run(){
System.out.println("林:刚美完容,睡一会吧~");
try {
Thread.sleep(9999999);
} catch (InterruptedException e) {
System.out.println("林:干嘛呢!干嘛呢!干嘛呢!都破了像了!");
}
System.out.println("林:醒了");
}
};
Thread huang = new Thread(){
public void run(){
System.out.println("黄:大锤80!小锤40!开始砸墙!");
for(int i=0;i<5;i++){
System.out.println("黄:80!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
System.out.println("咣当!");
System.out.println("黄:大哥,搞定!");
lin.interrupt();//中断lin的睡眠阻塞
}
};
lin.start();
huang.start();
}
}
守护线程:Thread setDaemon(boolean on);
护线程也称为:后台线程;守护线程是通过普通线程调用setDaemon(boolean on)方法设置而来的,因此创建上与普通线程无异;守护线程的结束时机上有一点与普通线程不同,即:进程的结束:当一个进程中的所有普通线程都结束时,进程就会结束,此时会杀掉所有正在运行的守护线程;
通常当我们不关心某个线程的任务什么时候停下来,它可以一直运行,但是程序主要的工作都结束时它应当跟着结束时,这样的任务就适合放在守护线程上执行;比如GC就是在守护线程上运行的;
public class DaemonThreadDemo {
public static void main(String[] args) {
Thread rose = new Thread(){
public void run(){
for(int i=0;i<5;i++){
System.out.println("rose:let me go!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
System.out.println("rose:啊啊啊啊啊啊AAAAAAAaaaaa....");
System.out.println("噗通");
}
};
Thread jack = new Thread(){
public void run(){
while(true){
System.out.println("jack:you jump!i jump!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
}
};
rose.start();
jack.setDaemon(true);//设置守护线程必须在线程启动前进行
jack.start();
}
}
多线程并发安全问题:
当多个线程并发操作同一临界资源,由于线程切换时机不确定,导致操作临界资源的顺序出现混乱严重时可能导致系统瘫痪;
临界资源:指该资源的全过程同时只能被单个线程完成;
并发安全解决办法之一:使用synchronize修饰:当一个方法使用synchronized修饰后,这个方法称为"同步方法",即:多个线程不能同时 在方法内部执行.只能有先后顺序的一个一个进行. 将并发操作同一临界资源的过程改为同步执行就可以有效的解决并发安全问题;
使用synchronize的两种方式:
一、在方法上修饰,此时该方法变为一个同步方法;
二、同步块,可以更准确的锁定需要排队的代码片段:有效的缩小同步范围可以在保证并发安全的前提下尽可能提高并发效率;
同步监视器对象的选取:
对于同步的成员方法而言,同步监视器对象不可指定,只能是this
对于同步的静态方法而言,同步监视器对象也不可指定,只能是类对象
对于同步块而言,需要自行指定同步监视器对象,选取原则:
1.必须是引用类型
2.多个需要同步执行该同步块的线程看到的该对象必须是同一个
public class SyncDemo2 {
public static void main(String[] args) {
// Shop shop1 = new Shop();
// Shop shop2 = new Shop();
Shop shop = new Shop();
Thread t1 = new Thread("张三"){
public void run(){
// shop1.buy();
shop.buy();
}
};
Thread t2 = new Thread("李四"){
public void run(){
// shop2.buy();
shop.buy();
}
};
t1.start();
t2.start();
}
}
class Shop{
/*
在成员方法上使用synchronized时,同步监视器对象就是当前方法的所属对象this。
*/
// public synchronized void buy(){
public void buy(){
try {
Thread t = Thread.currentThread();//获取运行buy方法的线程
System.out.println(t.getName()+":正在挑衣服...");
Thread.sleep(5000);
/*
在使用同步块时要指定同步监视器对象(上锁的对象)
该对象必须同时满足以下两个条件:
1:必须是一个引用类型
2:多个需要同步执行该代码片段的线程看到的这个锁对象必须为【同一个】!
*/
synchronized (this) {//可以,因为t1和t2调用的是同一个shop的buy方法,因此方法内的
this是同一个shop对象
// synchronized (t) {//不可以,因为t1和t2调用buy方法时获取的就是线程自身,因此t是不
同的线程。
// synchronized (new Object()) {//不可以,有new一定不行!因为new一定产生新对象
/*
"hello"(字符串字面量)作为锁对象,由于java对字符串的优化机制,字面量
始终是同一个对象,这会导致如果t1和t2线程调用不同shop的buy方法(相当于两个人
进的不同商店买衣服)但由于在这里看到的锁对象相同,则仍然需要排队执行,这就是
不可理的操作了。排队执行的前提是存在并发安全问题(有"抢"这件事发生)。
*/
// synchronized ("hello") {
System.out.println(t.getName() + ":正在试衣服...");
Thread.sleep(5000);
}
System.out.println(t.getName()+":结账离开");
} catch (InterruptedException e) {
}
}
}
在静态方法上使用synchronized:当在静态方法上使用synchronized后,该方法是一个同步方法.由于静态方法所属类,所以一定具有同步效果;静态方法使用的同步监视器对象为当前类的类对象(Class的实例),在JVM中,每个被加载的类都有且只有一个Class的实例与之对应;
互斥锁
当多个线程执行不同的代码片段,但是这些代码片段之间不能同时运行时就要设置为互斥的;使用synchronized锁定多个代码片段,并且指定的同步监视器是同一个时,这些代码片段之间就是互斥的。
创建一个可以多人聊天的服务端与客户端,并解决多线程安全问题:
服务端:
public class Ser {
private Socket socket;
Collection<PrintWriter> pwArr = new ArrayList<>();
private ServerSocket serverSocket;
public static void main(String[] args) {
Ser ser = new Ser();
ser.serRun();
}
public Ser(){
try {
serverSocket = new ServerSocket(8088);
System.out.println("初始化服务端成功");
} catch (IOException e) {
e.printStackTrace();
}
}
public void serRun() {
try {
while (true) {
socket = serverSocket.accept();
Runnable serverHandler = new ServerHandler(socket);
Thread t1 = new Thread(serverHandler);
t1.start();
}
} catch (IOException e) {
}
}
class ServerHandler implements Runnable{
private String host;
private Socket socket;
private PrintWriter pw;
ServerHandler(Socket socket){
this.socket = socket;
host = socket.getInetAddress().getHostAddress();
}
@Override
public void run() {
try {
InputStream is = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr);
OutputStream os = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(os,StandardCharsets.UTF_8);
BufferedWriter bw = new BufferedWriter(osw);
pw = new PrintWriter(bw,true);
synchronized(pwArr){
pwArr.add(pw);
}
sendMessage(host+"上线了,"+"当前在线人数为"+pwArr.size());
String words;
while((words = br.readLine())!=null){
sendMessage(host+":"+words);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
synchronized (pwArr){
pwArr.remove(pw);
}
sendMessage(host+"下线了,"+"当前在线人数为"+pwArr.size());
try {
socket.close();
} catch (IOException e) {
}
}
}
public void sendMessage(String line){
System.out.println(line);
for(PrintWriter p : pwArr){
p.println(line);
}
}
}
客户端:
private Socket socket;
Scanner sc = new Scanner(System.in);
public static void main(String[] args) {
Cli cli = new Cli();
cli.run();
}
Cli(){
System.out.println("初始化客户端");
try {
socket = new Socket("localhost",8088);
System.out.println("客户端初始化成功");
} catch (IOException e) {
e.printStackTrace();
}
}
public void run(){
try {
ClientHandler ch = new ClientHandler();
Thread t1 = new Thread(ch);
t1.setDaemon(true);
t1.start();
System.out.println("请打字");
OutputStream os = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(os, StandardCharsets.UTF_8);
BufferedWriter bw = new BufferedWriter(osw);
PrintWriter pw = new PrintWriter(bw,true);
while(true){
String word = sc.nextLine();
if("exit".equals(word)){
break;
}
pw.println(word);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
class ClientHandler implements Runnable{
@Override
public void run() {
try {
InputStream is = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
String line ;
//注意不能在这里直接赋值br.readline() String line,否则会死循环(线程里的程序是循环的)
while((line = br.readLine())!=null){
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
4、集合
集合与数组一样,可以保存一组元素,并且提供了操作元素的相关方法,使用更方便;集合只能存放引用类型元素,并且存放的是元素的引用;
java.util.Collection接口:是所有集合的顶级接口,它有以下两种常见的子接口:
java.util.List:线性表,是可重复集合,有序;这里可重复指的是集合中的元素是否可以重复,而判定重复元素的标准是依靠元素自身equals比较的结果,为true就认为是重复元素;
java.util.Set:不可重复集合,大部分实现类都是无序的;
Collection常见API:
boolean b = c.add(E e) :向当前集合中添加一个元素.当元素成功添加后返回true;
int t = c.size() :返回当前集合的元素个数;
boolean b = c.isEmpty():判断当前集合是否为空集(不含有任何元素);
c.clear():清空集合c;
c.toArray():返回一个包含集合c所有元素的数组;
c.toArray(T[ ] a):返回包含此集合中所有元素的数组; 返回的数组的运行时类型是指定数组的运行时类型。 如果集合适合指定的数组,则返回其中。 否则,将为指定数组的运行时类型和此集合的大小分配一个新数组。
与equals()相关的方法:
boolean b = c.contains(Object o):判断当前集合是否包含给定元素,这里判断的依据是给定元素是否与集合现有元素存在equals比较为true的情况;
c.remove(p):用来从集合中删除给定元素,删除的也是与集合中equals比较为true的元素。注意,对于可以存放重复元素的集合而言,只删除一次;
集合之间的操作方法:
boolean b = c.addAll(Collection c1):将c1集合中的所有元素添加到c集合中;
boolean b = c.containsAll(Collection c1):判断c集合是否包含c1的所有元素;
boolean b = c.retainAll(Collextion c1):c集合取与c1集合的交集,操作成功则发返回true;
boolean b = c.removeAll(Collection c1):将c集合中所有包含c1集合的元素删除;
集合的遍历:迭代器模式
Iterator it = c.iterator():不同的集合都实现了一个用于遍历自身元素的迭代器实现类,我们无需记住它们的名字,用多态的角度把他们看做为Iterator即可;
迭代器遍历集合遵循的步骤为:问->取->删;其中删除不是必须操作。
while(it.hasNext()){ //问
String str = (String)it.next(); //取
System.out.println(str);
}
注意:迭代器遍历过程中不得通过集合的方法增删元素,否则会:ConcurrentModificationException
增强型for循环:JDK5推出时,推出了一个新的特性:增强型for循环,也称为新循环,它可以用相同的语法遍历集合或数组。新循环是java编译器认可的,并非虚拟机。
public static void main(String[] args) {
String[] array = {"one","two","three","four","five"};
for(int i=0;i<array.length;i++){
String str = array[i];
System.out.println(str);
}
for(String str : array){
System.out.println(str);
}
泛型:JDK5之后推出的另一个特性,泛型也称为参数化类型,允许我们在使用一个类时指定它当中属性、方法参数或返回值的类型;泛型在集合中被广泛使用,用来指定集合中的元素类型;有泛型支持的类在使用时若不指定泛型的具体类型则默认为原型Object;
编译器会检查调用方法的实参是否为泛型指定的String类型:
Collection<String> c = new ArrayList<>()
Iterator<String> it = c.iterator();
List型集合的常见实现类:是可重复集,并且有序,提供了一套可以通过下标操作元素的方法
- java.util.ArrayList:内部使用数组实现,查询性能更好;
- java.util.LinkedList:内部使用链表实现,首尾增删元素性能更好;
List型常见方法:
list.get(int index):获取指定下标处的元素;
list.set(int index,E e):将给定元素设置到指定位置,返回值为该位置原有的元素;
list.subList(int start,int end):截取集合指定范围内的元素(含头不含尾),返回一个新的List集合,
注意该新集合可以看做是原集合的映射,在新集合上的所有操作都会包含旧集合;
List<Integer> list = new ArrayList<>();
for(int i=0;i<10;i++){
list.add(i);
}
List<Integer> subList = list.subList(3,8);
System.out.println(subList); //[3, 4, 5, 6, 7]
System.out.println(list); //[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
for(int i=0;i<subList.size();i++){
subList.set(i,subList.get(i)*10);
}
System.out.println(subList); //[30, 40, 50, 60, 70]
System.out.println(list); //[0, 1, 2, 30, 40, 50, 60, 70, 8, 9]
/*
sublist()可以看做是对原集合的映射,对截取的集合操作就是对原集合操作
*/
subList.clear();
System.out.println(list); //[0, 1, 2, 8, 9]
List集合提供了一对重载的add,remove方法:
add(int index, E e):在指定下标处插入指定元素;
remove(int index):删除并返回在指定下标处的元素;