Java中级语法_java中级语法 csdn-程序员宅基地

技术标签: java  

目录

二、Java中级

1.1 异常处理

1、什么是异常

2、处理

3、异常分类

4、Throwable

5、自定义异常

1.2 I/O

1、文件对象

2、什么是流

3、字节流

4、关闭流的方式

5、字符流

6、中文问题

7、缓存流

8、数据流

9、对象流

10、System.in

 11、I/O流关系图

1.3 集合框架

1、ArrayList

2、其他集合

3、关系与区别

4、其他

 1.4 泛型

1、集合中的泛型

2、支持泛型的类

3、通配符

4、泛型转型

 1.5 LAMBDA

1、Hello Lambda

2、方法引用

3、聚合操作

1.6 多线程

1、启动一个线程

2、常见线程方法

3、同步

4、线程安全的类

5、死锁

 6、交互

7、线程池

8、Lock对象

9、原子访问

 1.7 JDBC

1、MySQL

3、增、删、改

4、查询

 5、预编译Statement

6、execute executeUpdate

7、特殊操作

8、事务

9、ORM

10、DAO

11、数据库连接池

1.8 图形界面

1、Hello Swing

2、事件监听

3、容器

4、布局器

5、组件

 6、面板

7、菜单

8、工具栏

9、表格

10、日期控件

11、Swing中的线程

12、皮肤

1.9 网络编程

1、IP地址 端口

2、Socket

3、多线程聊天


二、Java中级

1.1 异常处理

1、什么是异常

异常定义:
导致程序的正常流程被中断的事件,叫做异常

文件不存在异常

比如要打开d盘的LOL.exe文件,这个文件是有可能不存在的
Java中通过 new FileInputStream(f) 试图打开某文件,就有可能抛出文件不存在异常FileNotFoundException
如果不处理该异常,就会有编译错误
处理办法参见 异常处理

package exception;
 
import java.io.File;
import java.io.FileInputStream;
 
public class TestException {
 
    public static void main(String[] args) {
         
        File f= new File("d:/LOL.exe");
         
        //试图打开文件LOL.exe,会抛出FileNotFoundException,如果不处理该异常,就会有编译错误
        new FileInputStream(f);
         
    }
}

2、处理

异常处理常见手段: try catch finally throws

try catch

1.将可能抛出FileNotFoundException 文件不存在异常的代码放在try里
2.如果文件存在,就会顺序往下执行,并且不执行catch块中的代码
3. 如果文件不存在,try 里的代码会立即终止,程序流程会运行到对应的catch块中
4. e.printStackTrace(); 会打印出方法的调用痕迹,如此例,会打印出异常开始于TestException的第16行,这样就便于定位和分析到底哪里出了异常

package exception;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;

public class TestException {

	public static void main(String[] args) {
		
		File f= new File("d:/LOL.exe");
		
		try{
			System.out.println("试图打开 d:/LOL.exe");
			new FileInputStream(f);
			System.out.println("成功打开");
		}
		catch(FileNotFoundException e){
			System.out.println("d:/LOL.exe不存在");
			e.printStackTrace();
		}
		
	}
}

使用异常的父类进行catch

FileNotFoundException是Exception的子类,使用Exception也可以catch住FileNotFoundException

package exception;
 
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
 
public class TestException {
 
    public static void main(String[] args) {
         
        File f= new File("d:/LOL.exe");
         
        try{
            System.out.println("试图打开 d:/LOL.exe");
            new FileInputStream(f);
            System.out.println("成功打开");
        }
        
        catch(Exception e){
            System.out.println("d:/LOL.exe不存在");
            e.printStackTrace();
        }
         
    }
}

多异常捕捉方法1

有的时候一段代码会抛出多种异常,比如

new FileInputStream(f);

Date d = sdf.parse("2016-06-03");


这段代码,会抛出 文件不存在异常 FileNotFoundException 和 解析异常ParseException
解决办法之一是分别进行catch

catch (FileNotFoundException e) {

System.out.println("d:/LOL.exe不存在");

e.printStackTrace();

} catch (ParseException e) {

System.out.println("日期格式解析错误");

e.printStackTrace();

}

package exception;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class TestException {

	public static void main(String[] args) {

		File f = new File("d:/LOL.exe");

		try {
			System.out.println("试图打开 d:/LOL.exe");
			new FileInputStream(f);
			System.out.println("成功打开");
			SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
			Date d = sdf.parse("2016-06-03");
		} catch (FileNotFoundException e) {
			System.out.println("d:/LOL.exe不存在");
			e.printStackTrace();
		} catch (ParseException e) {
			System.out.println("日期格式解析错误");
			e.printStackTrace();
		}
	}
}

多异常捕捉方法2

另一个种办法是把多个异常,放在一个catch里统一捕捉

catch (FileNotFoundException | ParseException e) {


这种方式从 JDK7开始支持,好处是捕捉的代码更紧凑,不足之处是,一旦发生异常,不能确定到底是哪种异常,需要通过instanceof 进行判断具体的异常类型

if (e instanceof FileNotFoundException)

System.out.println("d:/LOL.exe不存在");

if (e instanceof ParseException)

System.out.println("日期格式解析错误");

package exception;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class TestException {

	public static void main(String[] args) {

		File f = new File("d:/LOL.exe");

		try {
			System.out.println("试图打开 d:/LOL.exe");
			new FileInputStream(f);
			System.out.println("成功打开");
			SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
			Date d = sdf.parse("2016-06-03");
		} catch (FileNotFoundException | ParseException e) {
			if (e instanceof FileNotFoundException)
				System.out.println("d:/LOL.exe不存在");
			if (e instanceof ParseException)
				System.out.println("日期格式解析错误");

			e.printStackTrace();
		}

	}
}

finally

无论是否出现异常,finally中的代码都会被执行

package exception;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;

public class TestException {

	public static void main(String[] args) {
		
		File f= new File("d:/LOL.exe");
		
		try{
			System.out.println("试图打开 d:/LOL.exe");
			new FileInputStream(f);
			System.out.println("成功打开");
		}
		catch(FileNotFoundException e){
			System.out.println("d:/LOL.exe不存在");
			e.printStackTrace();
		}
		finally{
			System.out.println("无论文件是否存在, 都会执行的代码");
		}
	}
}

throws

考虑如下情况:
主方法调用method1
method1调用method2
method2中打开文件

method2中需要进行异常处理
但是method2不打算处理,而是把这个异常通过throws抛出去
那么method1就会接到该异常。 处理办法也是两种,要么是try catch处理掉,要么也是抛出去。
method1选择本地try catch住 一旦try catch住了,就相当于把这个异常消化掉了,主方法在调用method1的时候,就不需要进行异常处理了

package exception;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;

public class TestException {

	public static void main(String[] args) {
		method1();

	}

	private static void method1() {
		try {
			method2();
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

	private static void method2() throws FileNotFoundException {

		File f = new File("d:/LOL.exe");

		System.out.println("试图打开 d:/LOL.exe");
		new FileInputStream(f);
		System.out.println("成功打开");

	}
}

throw和throws的区别

throws与throw这两个关键字接近,不过意义不一样,有如下区别:
1. throws 出现在方法声明上,而throw通常都出现在方法体内。
2. throws 表示出现异常的一种可能性,并不一定会发生这些异常;throw则是抛出了异常,执行throw则一定抛出了某个异常对象。

3、异常分类

异常分类: 可查异常,运行时异常和错误3种
其中,运行时异常和错误又叫非可查异常

可查异常

可查异常: CheckedException
可查异常即必须进行处理的异常,要么try catch住,要么往外抛,谁调用,谁处理,比如 FileNotFoundException
如果不处理,编译器,就不让你通过

package exception;
 
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
 
public class TestException {
 
    public static void main(String[] args) {
         
        File f= new File("d:/LOL.exe");
         
        try{
            System.out.println("试图打开 d:/LOL.exe");
            new FileInputStream(f);
            System.out.println("成功打开");
        }
        catch(FileNotFoundException e){
            System.out.println("d:/LOL.exe不存在");
            e.printStackTrace();
        }
         
    }
}

运行时异常

运行时异常RuntimeException指: 不是必须进行try catch的异常
常见运行时异常:
除数不能为0异常:ArithmeticException
下标越界异常:ArrayIndexOutOfBoundsException
空指针异常:NullPointerException
在编写代码的时候,依然可以使用try catch throws进行处理,与可查异常不同之处在于,即便不进行try catch,也不会有编译错误
Java之所以会设计运行时异常的原因之一,是因为下标越界,空指针这些运行时异常太过于普遍,如果都需要进行捕捉,代码的可读性就会变得很糟糕。

package exception;
 
public class TestException {
 
    public static void main(String[] args) {
    	
    	//任何除数不能为0:ArithmeticException
    	int k = 5/0; 
    	
    	//下标越界异常:ArrayIndexOutOfBoundsException
    	int j[] = new int[5];
    	j[10] = 10;
    	
    	//空指针异常:NullPointerException
    	String str = null;
    	str.length();
   }
}

错误

错误Error,指的是系统级别的异常,通常是内存用光了
在默认设置下,一般java程序启动的时候,最大可以使用16m的内存
如例不停的给StringBuffer追加字符,很快就把内存使用光了。抛出OutOfMemoryError
与运行时异常一样,错误也是不要求强制捕捉的

package exception;
 
public class TestException {
 
    public static void main(String[] args) {
    
    	StringBuffer sb =new StringBuffer();
    	
    	for (int i = 0; i < Integer.MAX_VALUE; i++) {
			sb.append('a');
		}
    	
    }

}

三种分类

总体上异常分三类:
1. 错误
2. 运行时异常
3. 可查异常

4、Throwable

Throwable

Throwable是类,Exception和Error都继承了该类
所以在捕捉的时候,也可以使用Throwable进行捕捉
如图: 异常分Error和Exception
Exception里又分运行时异常和可查异常。

package exception;

import java.io.File;
import java.io.FileInputStream;

public class TestException {

	public static void main(String[] args) {

		File f = new File("d:/LOL.exe");

		try {
			new FileInputStream(f);
			//使用Throwable进行异常捕捉
		} catch (Throwable t) {
			// TODO Auto-generated catch block
			t.printStackTrace();
		}

	}
}

5、自定义异常

创建自定义异常

一个英雄攻击另一个英雄的时候,如果发现另一个英雄已经挂了,就会抛出EnemyHeroIsDeadException
创建一个类EnemyHeroIsDeadException,并继承Exception
提供两个构造方法
1. 无参的构造方法
2. 带参的构造方法,并调用父类的对应的构造方法

class EnemyHeroIsDeadException extends Exception{
    
	public EnemyHeroIsDeadException(){
		
	}
    public EnemyHeroIsDeadException(String msg){
        super(msg);
    }
}

抛出自定义异常

在Hero的attack方法中,当发现敌方英雄的血量为0的时候,抛出该异常
1. 创建一个EnemyHeroIsDeadException实例
2. 通过throw 抛出该异常
3. 当前方法通过 throws 抛出该异常

在外部调用attack方法的时候,就需要进行捕捉,并且捕捉的时候,可以通过e.getMessage() 获取当时出错的具体原因

package charactor;
 
public class Hero {
    public String name; 
    protected float hp;

    public void attackHero(Hero h) throws EnemyHeroIsDeadException{
    	if(h.hp == 0){
    		throw new EnemyHeroIsDeadException(h.name + " 已经挂了,不需要施放技能" );
    	}
    }

    public String toString(){
    	return name;
    }
    
    class EnemyHeroIsDeadException extends Exception{
        
    	public EnemyHeroIsDeadException(){
    		
    	}
        public EnemyHeroIsDeadException(String msg){
            super(msg);
        }
    }
     
    public static void main(String[] args) {
    	
        Hero garen =  new Hero();
        garen.name = "盖伦";
        garen.hp = 616;

        Hero teemo =  new Hero();
        teemo.name = "提莫";
        teemo.hp = 0;
        
        try {
			garen.attackHero(teemo);
			
		} catch (EnemyHeroIsDeadException e) {
			// TODO Auto-generated catch block
			System.out.println("异常的具体原因:"+e.getMessage());
			e.printStackTrace();
		}
        
    }
}

1.2 I/O

1、文件对象

文件和文件夹都是用File代表

创建一个文件对象

使用绝对路径或者相对路径创建File对象

package file;
 
import java.io.File;
 
public class TestFile {
 
    public static void main(String[] args) {
        // 绝对路径
        File f1 = new File("d:/LOLFolder");
        System.out.println("f1的绝对路径:" + f1.getAbsolutePath());
        // 相对路径,相对于工作目录,如果在eclipse中,就是项目目录
        File f2 = new File("LOL.exe");
        System.out.println("f2的绝对路径:" + f2.getAbsolutePath());
 
        // 把f1作为父目录创建文件对象
        File f3 = new File(f1, "LOL.exe");
 
        System.out.println("f3的绝对路径:" + f3.getAbsolutePath());
    }
}

文件常用方法1

注意1: 需要在D:\LOLFolder确实存在一个LOL.exe,才可以看到对应的文件长度、修改时间等信息

注意2: renameTo方法用于对物理文件名称进行修改,但是并不会修改File对象的name属性。

package file;
 
import java.io.File;
import java.util.Date;
 
public class TestFile {
 
    public static void main(String[] args) {
 
        File f = new File("d:/LOLFolder/LOL.exe");
        System.out.println("当前文件是:" +f);
        //文件是否存在
        System.out.println("判断是否存在:"+f.exists());
        
        //是否是文件夹
        System.out.println("判断是否是文件夹:"+f.isDirectory());
         
        //是否是文件(非文件夹)
        System.out.println("判断是否是文件:"+f.isFile());
         
        //文件长度
        System.out.println("获取文件的长度:"+f.length());
         
        //文件最后修改时间
        long time = f.lastModified();
        Date d = new Date(time);
        System.out.println("获取文件的最后修改时间:"+d);
        //设置文件修改时间为1970.1.1 08:00:00
        f.setLastModified(0);
         
        //文件重命名
        File f2 =new File("d:/LOLFolder/DOTA.exe");
        f.renameTo(f2);
        System.out.println("把LOL.exe改名成了DOTA.exe");
        
        System.out.println("注意: 需要在D:\\LOLFolder确实存在一个LOL.exe,\r\n才可以看到对应的文件长度、修改时间等信息");
    }
}

文件常用方法2

package file;
 
import java.io.File;
import java.io.IOException;
 
public class TestFile {
 
    public static void main(String[] args) throws IOException {
 
        File f = new File("d:/LOLFolder/skin/garen.ski");
 
        // 以字符串数组的形式,返回当前文件夹下的所有文件(不包含子文件及子文件夹)
        f.list();
 
        // 以文件数组的形式,返回当前文件夹下的所有文件(不包含子文件及子文件夹)
        File[]fs= f.listFiles();
 
        // 以字符串形式返回获取所在文件夹
        f.getParent();
 
        // 以文件形式返回获取所在文件夹
        f.getParentFile();
        // 创建文件夹,如果父文件夹skin不存在,创建就无效
        f.mkdir();
 
        // 创建文件夹,如果父文件夹skin不存在,就会创建父文件夹
        f.mkdirs();
 
        // 创建一个空文件,如果父文件夹skin不存在,就会抛出异常
        f.createNewFile();
        // 所以创建一个空文件之前,通常都会创建父目录
        f.getParentFile().mkdirs();
 
        // 列出所有的盘符c: d: e: 等等
        f.listRoots();
 
        // 刪除文件
        f.delete();
 
        // JVM结束的时候,刪除文件,常用于临时文件的删除
        f.deleteOnExit();
 
    }
}

2、什么是流

什么是流(Stream),流就是一系列的数据

什么是流

当不同的介质之间有数据交互的时候,JAVA就使用流来实现。
数据源可以是文件,还可以是数据库,网络甚至是其他的程序


比如读取文件的数据到程序中,站在程序的角度来看,就叫做输入流
输入流: InputStream
输出流:OutputStream

文件输入流

如下代码,就建立了一个文件输入流,这个流可以用来把数据从硬盘的文件,读取到JVM(内存)。

目前代码只是建立了流,还没有开始读取,真正的读取在下个章节讲解。

package stream;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class TestStream {

	public static void main(String[] args) {
		try {
			File f = new File("d:/lol.txt");
			// 创建基于文件的输入流
			FileInputStream fis = new FileInputStream(f);
			// 通过这个输入流,就可以把数据从硬盘,读取到Java的虚拟机中来,也就是读取到内存中

		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}
}

3、字节流

InputStream字节输入流
OutputStream字节输出流
用于以字节的形式读取和写入数据

ASCII码概念

所有的数据存放在计算机中都是以数字的形式存放的。 所以字母就需要转换为数字才能够存放。
比如A就对应的数字65,a对应的数字97. 不同的字母和符号对应不同的数字,就是一张码表。
ASCII是这样的一种码表。 只包含简单的英文字母,符号,数字等等。 不包含中文,德文,俄语等复杂的。

示例中列出了可见的ASCII码以及对应的十进制和十六进制数字,不可见的暂未列出

 

以字节流的形式读取文件内容

InputStream是字节输入流,同时也是抽象类,只提供方法声明,不提供方法的具体实现。
FileInputStream 是InputStream子类,以FileInputStream 为例进行文件读取

package stream;
 
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
 
public class TestStream {
 
    public static void main(String[] args) {
        try {
            //准备文件lol.txt其中的内容是AB,对应的ASCII分别是65 66
            File f =new File("d:/lol.txt");
            //创建基于文件的输入流
            FileInputStream fis =new FileInputStream(f);
            //创建字节数组,其长度就是文件的长度
            byte[] all =new byte[(int) f.length()];
            //以字节流的形式读取文件所有内容
            fis.read(all);
            for (byte b : all) {
                //打印出来是65 66
                System.out.println(b);
            }
            
            //每次使用完流,都应该进行关闭
            fis.close();
             
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
         
    }
}

以字节流的形式向文件写入数据

OutputStream是字节输出流,同时也是抽象类,只提供方法声明,不提供方法的具体实现。
FileOutputStream 是OutputStream子类,以FileOutputStream 为例向文件写出数据

注: 如果文件d:/lol2.txt不存在,写出操作会自动创建该文件。
但是如果是文件 d:/xyz/lol2.txt,而目录xyz又不存在,会抛出异常

package stream;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

public class TestStream {

	public static void main(String[] args) {
		try {
			// 准备文件lol2.txt其中的内容是空的
			File f = new File("d:/lol2.txt");
			// 准备长度是2的字节数组,用88,89初始化,其对应的字符分别是X,Y
			byte data[] = { 88, 89 };

			// 创建基于文件的输出流
			FileOutputStream fos = new FileOutputStream(f);
			// 把数据写入到输出流
			fos.write(data);
			// 关闭输出流
			fos.close();
			
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}
}

4、关闭流的方式

所有的流,无论是输入流还是输出流,使用完毕之后,都应该关闭。 如果不关闭,会产生对资源占用的浪费。 当量比较大的时候,会影响到业务的正常开展。

在try中关闭

在try的作用域里关闭文件输入流,在前面的示例中都是使用这种方式,这样做有一个弊端;
如果文件不存在,或者读取的时候出现问题而抛出异常,那么就不会执行这一行关闭流的代码,存在巨大的资源占用隐患。 不推荐使用

package stream;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class TestStream {

	public static void main(String[] args) {
		try {
			File f = new File("d:/lol.txt");
			FileInputStream fis = new FileInputStream(f);
			byte[] all = new byte[(int) f.length()];
			fis.read(all);
			for (byte b : all) {
				System.out.println(b);
			}
			// 在try 里关闭流
			fis.close();
		} catch (IOException e) {
			e.printStackTrace();
		}

	}
}

在finally中关闭

这是标准的关闭流的方式
1. 首先把流的引用声明在try的外面,如果声明在try里面,其作用域无法抵达finally.
2. 在finally关闭之前,要先判断该引用是否为空
3. 关闭的时候,需要再一次进行try catch处理

这是标准的严谨的关闭流的方式,但是看上去很繁琐,所以写不重要的或者测试代码的时候,都会采用上面的有隐患try的方式,因为不麻烦~

package stream;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class TestStream {

	public static void main(String[] args) {
		File f = new File("d:/lol.txt");
		FileInputStream fis = null;
		try {
			fis = new FileInputStream(f);
			byte[] all = new byte[(int) f.length()];
			fis.read(all);
			for (byte b : all) {
				System.out.println(b);
			}

		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			// 在finally 里关闭流
			if (null != fis)
				try {

					fis.close();
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
		}

	}
}

使用try()的方式

把流定义在try()里,try,catch或者finally结束的时候,会自动关闭
这种编写代码的方式叫做 try-with-resources, 这是从JDK7开始支持的技术

所有的流,都实现了一个接口叫做 AutoCloseable,任何类实现了这个接口,都可以在try()中进行实例化。 并且在try, catch, finally结束的时候自动关闭,回收相关资源。

package stream;
 
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
 
public class TestStream {
 
    public static void main(String[] args) {
        File f = new File("d:/lol.txt");
 
        //把流定义在try()里,try,catch或者finally结束的时候,会自动关闭
        try (FileInputStream fis = new FileInputStream(f)) {
            byte[] all = new byte[(int) f.length()];
            fis.read(all);
            for (byte b : all) {
                System.out.println(b);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
 
    }
}

5、字符流

Reader字符输入流
Writer字符输出流
专门用于字符的形式读取和写入数据

使用字符流读取文件

FileReader 是Reader子类,以FileReader 为例进行文件读取

package stream;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;

public class TestStream {

	public static void main(String[] args) {
		// 准备文件lol.txt其中的内容是AB
		File f = new File("d:/lol.txt");
		// 创建基于文件的Reader
		try (FileReader fr = new FileReader(f)) {
			// 创建字符数组,其长度就是文件的长度
			char[] all = new char[(int) f.length()];
			// 以字符流的形式读取文件所有内容
			fr.read(all);
			for (char b : all) {
				// 打印出来是A B
				System.out.println(b);
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}
}

使用字符流把字符串写入到文件

FileWriter 是Writer的子类,以FileWriter 为例把字符串写入到文件

package stream;
 
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
 
public class TestStream {
 
    public static void main(String[] args) {
        // 准备文件lol2.txt
        File f = new File("d:/lol2.txt");
        // 创建基于文件的Writer
        try (FileWriter fr = new FileWriter(f)) {
            // 以字符流的形式把数据写入到文件中
        	String data="abcdefg1234567890";
        	char[] cs = data.toCharArray();
            fr.write(cs);
 
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
 
    }
}

6、中文问题

编码概念

计算机存放数据只能存放数字,所有的字符都会被转换为不同的数字。
就像一个棋盘一样,不同的字,处于不同的位置,而不同的位置,有不同的数字编号。
有的棋盘很小,只能放数字和英文
有的大一点,还能放中文
有的“足够”大,能够放下世界人民所使用的所有文字和符号

如图所示,英文字符 A 能够放在所有的棋盘里,而且位置都差不多
中文字符, 中文字符 中 能够放在后两种棋盘里,并且位置不一样,而且在小的那个棋盘里,就放不下中文

常见编码

工作后经常接触的编码方式有如下几种:
ISO-8859-1 ASCII 数字和西欧字母
GBK GB2312 BIG5 中文
UNICODE (统一码,万国码)

其中
ISO-8859-1 包含 ASCII
GB2312 是简体中文,BIG5是繁体中文,GBK同时包含简体和繁体以及日文。
UNICODE 包括了所有的文字,无论中文,英文,藏文,法文,世界所有的文字都包含其中

UNICODE和UTF

根据前面的学习,我们了解到不同的编码方式对应不同的棋盘,而UNICODE因为要存放所有的数据,那么它的棋盘是最大的。
不仅如此,棋盘里每个数字都是很长的(4个字节),因为不仅要表示字母,还要表示汉字等。

如果完全按照UNICODE的方式来存储数据,就会有很大的浪费。
比如在ISO-8859-1中,a 字符对应的数字是0x61
而UNICODE中对应的数字是 0x00000061,倘若一篇文章大部分都是英文字母,那么按照UNICODE的方式进行数据保存就会消耗很多空间

在这种情况下,就出现了UNICODE的各种减肥子编码, 比如UTF-8对数字和字母就使用一个字节,而对汉字就使用3个字节,从而达到了减肥还能保证健康的效果

UTF-8,UTF-16和UTF-32 针对不同类型的数据有不同的减肥效果,一般说来UTF-8是比较常用的方式

UTF-8,UTF-16和UTF-32 彼此的区别在此不作赘述,有兴趣的可以参考 

unicode-百度

Java采用的是Unicode

写在.java源代码中的汉字,在执行之后,都会变成JVM中的字符。
而这些中文字符采用的编码方式,都是使用UNICODE. "中"字对应的UNICODE是4E2D,所以在内存中,实际保存的数据就是十六进制的0x4E2D, 也就是十进制的20013。

package stream;

public class TestStream {
	public static void main(String[] args) {
		String str = "中";
	}
}

一个汉字使用不同编码方式的表现

以字符 中 为例,查看其在不同编码方式下的值是多少

也即在不同的棋盘上的位置

package stream;

import java.io.UnsupportedEncodingException;

public class TestStream {

	public static void main(String[] args) {
		String str = "中";
		showCode(str);
	}

	private static void showCode(String str) {
		String[] encodes = { "BIG5", "GBK", "GB2312", "UTF-8", "UTF-16", "UTF-32" };
		for (String encode : encodes) {
			showCode(str, encode);
		}

	}

	private static void showCode(String str, String encode) {
		try {
			System.out.printf("字符: \"%s\" 的在编码方式%s下的十六进制值是%n", str, encode);
			byte[] bs = str.getBytes(encode);

			for (byte b : bs) {
				int i = b&0xff;
				System.out.print(Integer.toHexString(i) + "\t");
			}
			System.out.println();
			System.out.println();
		} catch (UnsupportedEncodingException e) {
			System.out.printf("UnsupportedEncodingException: %s编码方式无法解析字符%s\n", encode, str);
		}
	}
}

 文件的编码方式-记事本

接下来讲,字符在文件中的保存
字符保存在文件中肯定也是以数字形式保存的,即对应在不同的棋盘上的不同的数字
用记事本打开任意文本文件,并且另存为,就能够在编码这里看到一个下拉。
ANSI 这个不是ASCII的意思,而是采用本地编码的意思。如果你是中文的操作系统,就会使GBK,如果是英文的就会是ISO-8859-1
Unicode UNICODE原生的编码方式
Unicode big endian 另一个 UNICODE编码方式
UTF-8 最常见的UTF-8编码方式,数字和字母用一个字节, 汉字用3个字节。

 文件的编码方式-eclipse

eclipse也有类似的编码方式,右键任意文本文件,点击最下面的"property"
就可以看到Text file encoding
也有ISO-8859-1,GBK,UTF-8等等选项。
其他的US-ASCII,UTF-16,UTF-16BE,UTF-16LE不常用。

用FileInputStream字节流正确读取中文

为了能够正确的读取中文内容
1. 必须了解文本是以哪种编码方式保存字符的
2. 使用字节流读取了文本后,再使用对应的编码方式去识别这些数字,得到正确的字符
如本例,一个文件中的内容是字符中,编码方式是GBK,那么读出来的数据一定是D6D0。
再使用GBK编码方式识别D6D0,就能正确的得到字符中

注: 在GBK的棋盘上找到的中字后,JVM会自动找到中在UNICODE这个棋盘上对应的数字,并且以UNICODE上的数字保存在内存中

package stream;
  
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
  
public class TestStream {
  
    public static void main(String[] args) {
        File f = new File("E:\\project\\j2se\\src\\test.txt");
        try (FileInputStream fis = new FileInputStream(f);) {
            byte[] all = new byte[(int) f.length()];
            fis.read(all);
  
            //文件中读出来的数据是
            System.out.println("文件中读出来的数据是:");
            for (byte b : all) 
            {
                int i = b&0x000000ff;  //只取16进制的后两位
                System.out.println(Integer.toHexString(i));
            }
            System.out.println("把这个数字,放在GBK的棋盘上去:");
            String str = new String(all,"GBK");
            System.out.println(str);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
  
    }
}

用FileReader字符流正确读取中文

FileReader得到的是字符,所以一定是已经把字节根据某种编码识别成了字符了
而FileReader使用的编码方式是Charset.defaultCharset()的返回值,如果是中文的操作系统,就是GBK
FileReader是不能手动设置编码方式的,为了使用其他的编码方式,只能使用InputStreamReader来代替,像这样:

new InputStreamReader(new FileInputStream(f),Charset.forName("UTF-8"));


在本例中,用记事本另存为UTF-8格式,然后用UTF-8就能识别对应的中文了。

解释: 为什么中字前面有一个?
如果是使用记事本另存为UTF-8的格式,那么在第一个字节有一个标示符,叫做BOM用来标志这个文件是用UTF-8来编码的。

package stream;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;

public class TestStream {

	public static void main(String[] args) throws UnsupportedEncodingException, FileNotFoundException {
		File f = new File("E:\\project\\j2se\\src\\test.txt");
		System.out.println("默认编码方式:"+Charset.defaultCharset());
		//FileReader得到的是字符,所以一定是已经把字节根据某种编码识别成了字符了
		//而FileReader使用的编码方式是Charset.defaultCharset()的返回值,如果是中文的操作系统,就是GBK
		try (FileReader fr = new FileReader(f)) {
			char[] cs = new char[(int) f.length()];
			fr.read(cs);
			System.out.printf("FileReader会使用默认的编码方式%s,识别出来的字符是:%n",Charset.defaultCharset());
			System.out.println(new String(cs));
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		//FileReader是不能手动设置编码方式的,为了使用其他的编码方式,只能使用InputStreamReader来代替
		//并且使用new InputStreamReader(new FileInputStream(f),Charset.forName("UTF-8")); 这样的形式
		try (InputStreamReader isr = new InputStreamReader(new FileInputStream(f),Charset.forName("UTF-8"))) {
			char[] cs = new char[(int) f.length()];
			isr.read(cs);
			System.out.printf("InputStreamReader 指定编码方式UTF-8,识别出来的字符是:%n");
			System.out.println(new String(cs));
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
	}
}

7、缓存流

以介质是硬盘为例,字节流和字符流的弊端:
在每一次读写的时候,都会访问硬盘。 如果读写的频率比较高的时候,其性能表现不佳。

为了解决以上弊端,采用缓存流。
缓存流在读取的时候,会一次性读较多的数据到缓存中,以后每一次的读取,都是在缓存中访问,直到缓存中的数据读取完毕,再到硬盘中读取。

就好比吃饭,不用缓存就是每吃一口都到锅里去铲。用缓存就是先把饭盛到碗里,碗里的吃完了,再到锅里去铲

缓存流在写入数据的时候,会先把数据写入到缓存区,直到缓存区达到一定的量,才把这些数据,一起写入到硬盘中去。按照这种操作模式,就不会像字节流,字符流那样每写一个字节都访问硬盘,从而减少了IO操作

使用缓存流读取数据

缓存字符输入流 BufferedReader 可以一次读取一行数据

package stream;
 
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
 
public class TestStream {
 
    public static void main(String[] args) {
        // 准备文件lol.txt其中的内容是
        // garen kill teemo
        // teemo revive after 1 minutes
        // teemo try to garen, but killed again
        File f = new File("d:/lol.txt");
        // 创建文件字符流
        // 缓存流必须建立在一个存在的流的基础上
        try (
        		FileReader fr = new FileReader(f); 
        		BufferedReader br = new BufferedReader(fr);
        	) 
        {
            while (true) {
                // 一次读一行
                String line = br.readLine();
                if (null == line)
                    break;
                System.out.println(line);
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
 
    }
}

使用缓存流写出数据

PrintWriter 缓存字符输出流, 可以一次写出一行数据

package stream;
  
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
  
public class TestStream {
  
    public static void main(String[] args) {
        // 向文件lol2.txt中写入三行语句
        File f = new File("d:/lol2.txt");
         
        try (
                // 创建文件字符流
                FileWriter fw = new FileWriter(f);
                // 缓存流必须建立在一个存在的流的基础上               
                PrintWriter pw = new PrintWriter(fw);               
        ) {
            pw.println("garen kill teemo");
            pw.println("teemo revive after 1 minutes");
            pw.println("teemo try to garen, but killed again");
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
  
    }
}

flush

有的时候,需要立即把数据写入到硬盘,而不是等缓存满了才写出去。 这时候就需要用到flush

package stream;
   
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
public class TestStream {
    public static void main(String[] args) {
        //向文件lol2.txt中写入三行语句
        File f =new File("d:/lol2.txt");
        //创建文件字符流
        //缓存流必须建立在一个存在的流的基础上
        try(FileWriter fr = new FileWriter(f);PrintWriter pw = new PrintWriter(fr);) {
            pw.println("garen kill teemo");
            //强制把缓存中的数据写入硬盘,无论缓存是否已满
                pw.flush();            
            pw.println("teemo revive after 1 minutes");
                pw.flush();
            pw.println("teemo try to garen, but killed again");
                pw.flush();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

8、数据流

DataInputStream 数据输入流
DataOutputStream 数据输出流

直接进行字符串的读写

使用数据流的writeUTF()和readUTF() 可以进行数据的格式化顺序读写
如本例,通过DataOutputStream 向文件顺序写出 布尔值,整数和字符串。 然后再通过DataInputStream 顺序读入这些数据。

注: 要用DataInputStream 读取一个文件,这个文件必须是由DataOutputStream 写出的,否则会出现EOFException,因为DataOutputStream 在写出的时候会做一些特殊标记,只有DataInputStream 才能成功的读取。

package stream;
     
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
     
public class TestStream {
     
    public static void main(String[] args) {
    	write();
    	read();
    }

	private static void read() {
        File f =new File("d:/lol.txt");
		try (
                FileInputStream fis  = new FileInputStream(f);
                DataInputStream dis =new DataInputStream(fis);
        ){
            boolean b= dis.readBoolean();
            int i = dis.readInt();
            String str = dis.readUTF();
            
            System.out.println("读取到布尔值:"+b);
            System.out.println("读取到整数:"+i);
            System.out.println("读取到字符串:"+str);

        } catch (IOException e) {
            e.printStackTrace();
        }
		
	}

	private static void write() {
        File f =new File("d:/lol.txt");
		try (
                FileOutputStream fos  = new FileOutputStream(f);
                DataOutputStream dos =new DataOutputStream(fos);
        ){
            dos.writeBoolean(true);
            dos.writeInt(300);
            dos.writeUTF("123 this is gareen");
        } catch (IOException e) {
            e.printStackTrace();
        }
		
	}
}

9、对象流

对象流指的是可以直接把一个对象以流的形式传输给其他的介质,比如硬盘

一个对象以流的形式进行传输,叫做序列化。 该对象所对应的类,必须是实现Serializable接口

序列化一个对象

创建一个Hero对象,设置其名称为garen。
把该对象序列化到一个文件garen.lol。
然后再通过序列化把该文件转换为一个Hero对象

注:把一个对象序列化有一个前提是:这个对象的类,必须实现了Serializable接口

package stream;
   
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
 
import charactor.Hero;
   
public class TestStream {
   
    public static void main(String[] args) {
        //创建一个Hero garen
        //要把Hero对象直接保存在文件上,务必让Hero类实现Serializable接口
        Hero h = new Hero();
        h.name = "garen";
        h.hp = 616;
         
        //准备一个文件用于保存该对象
        File f =new File("d:/garen.lol");

        try(
	        //创建对象输出流
	        FileOutputStream fos = new FileOutputStream(f);
	        ObjectOutputStream oos =new ObjectOutputStream(fos);
        	//创建对象输入流        		
            FileInputStream fis = new FileInputStream(f);
            ObjectInputStream ois =new ObjectInputStream(fis);
        ) {
            oos.writeObject(h);
            Hero h2 = (Hero) ois.readObject();
            System.out.println(h2.name);
            System.out.println(h2.hp);
              
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
           
    }
}

10、System.in

System.out 是常用的在控制台输出数据的
System.in 可以从控制台输入数据

System.in

package stream;

import java.io.IOException;
import java.io.InputStream;

public class TestStream {

	public static void main(String[] args) {
		// 控制台输入
		try (InputStream is = System.in;) {
			while (true) {
				// 敲入a,然后敲回车可以看到
				// 97 13 10
				// 97是a的ASCII码
				// 13 10分别对应回车换行
				int i = is.read();
				System.out.println(i);
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

Scanner读取字符串

使用System.in.read虽然可以读取数据,但是很不方便
使用Scanner就可以逐行读取了

package stream;
   
import java.util.Scanner;
   
public class TestStream {
   
    public static void main(String[] args) {
        
        	Scanner s = new Scanner(System.in);
        	
        	while(true){
	        	String line = s.nextLine();
	        	System.out.println(line);
        	}
        
    }
}

Scanner从控制台读取整数

使用Scanner从控制台读取整数

package stream;

import java.util.Scanner;

public class TestStream {
	public static void main(String[] args) {
		Scanner s = new Scanner(System.in);
		int a = s.nextInt();
		System.out.println("第一个整数:"+a);
		int b = s.nextInt();
		System.out.println("第二个整数:"+b);
	}
}

 11、I/O流关系图

流关系图

这个图把本章节学到的流关系做了个简单整理
1. 流分为字节流和字符流
2. 字节流下面常用的又有数据流和对象流
3. 字符流下面常用的又有缓存流

 其他流

除了上图所接触的流之外,还有很多其他流,如图所示InputStream下面有很多的子类。 这些子类不需要立即掌握,他们大体上用法是差不多的,只是在一些特殊场合下用起来更方便,在工作中用到的时候再进行学习就行了。

1.3 集合框架

1、ArrayList

与数组的区别

使用数组的局限性

如果要存放多个对象,可以使用数组,但是数组有局限性
比如 声明长度是10的数组
不用的数组就浪费了
超过10的个数,又放不下

package collection;

import charactor.Hero;

public class TestCollection {
	public static void main(String[] args) {
		//数组的局限性
		Hero heros[] = new Hero[10];
		//声明长度是10的数组
		//不用的数组就浪费了
		//超过10的个数,又放不下
		heros[0] = new Hero("盖伦");
                //放不下要报错
		heros[20] = new Hero("提莫");
		
	}
	
}

ArrayList存放对象

 为了解决数组的局限性,引入容器类的概念。 最常见的容器类就是
ArrayList
容器的容量"capacity"会随着对象的增加,自动增长
只需要不断往容器里增加英雄即可,不用担心会出现数组的边界问题。

package collection;

import java.util.ArrayList;

import charactor.Hero;

public class TestCollection {
	@SuppressWarnings("rawtypes")
	public static void main(String[] args) {
		//容器类ArrayList,用于存放对象
		ArrayList heros = new ArrayList();
		heros.add( new Hero("盖伦"));
		System.out.println(heros.size());
		
		//容器的容量"capacity"会随着对象的增加,自动增长
		//只需要不断往容器里增加英雄即可,不用担心会出现数组的边界问题。
		heros.add( new Hero("提莫"));
		System.out.println(heros.size());
		
	}
	
}

常用方法

关键字 简介
add 增加
contains 判断是否存在
get 获取指定位置的对象
indexOf 获取对象所处的位置
remove 删除
set 替换
size 获取大小
toArray 转换为数组
addAll 把另一个容器所有对象都加进来
clear 清空

增加

add 有两种用法
第一种是直接add对象,把对象加在最后面

heros.add(new Hero("hero " + i));


第二种是在指定位置加对象

heros.add(3, specialHero);

package collection;

import java.util.ArrayList;

import charactor.Hero;

public class TestCollection {
	public static void main(String[] args) {
		ArrayList heros = new ArrayList();

		// 把5个对象加入到ArrayList中
		for (int i = 0; i < 5; i++) {
			heros.add(new Hero("hero " + i));
		}
		System.out.println(heros);

		// 在指定位置增加对象
		Hero specialHero = new Hero("special hero");
		heros.add(3, specialHero);

		System.out.println(heros.toString());

	}

}

判断是否存在

通过方法contains 判断一个对象是否在容器中
判断标准: 是否是同一个对象,而不是name是否相同

package collection;

import java.util.ArrayList;

import charactor.Hero;

public class TestCollection {
	public static void main(String[] args) {
		ArrayList heros = new ArrayList();

		// 初始化5个对象
		for (int i = 0; i < 5; i++) {
			heros.add(new Hero("hero " + i));
		}
		Hero specialHero = new Hero("special hero");
		heros.add(specialHero);

		System.out.println(heros);
		// 判断一个对象是否在容器中
		// 判断标准: 是否是同一个对象,而不是name是否相同
		System.out.print("虽然一个新的对象名字也叫 hero 1,但是contains的返回是:");
		System.out.println(heros.contains(new Hero("hero 1")));
		System.out.print("而对specialHero的判断,contains的返回是:");
		System.out.println(heros.contains(specialHero));
	}

}

 获取指定位置的对象 

通过get获取指定位置的对象,如果输入的下标越界,一样会报错

package collection;

import java.util.ArrayList;

import charactor.Hero;

public class TestCollection {
	public static void main(String[] args) {
		ArrayList heros = new ArrayList();

		// 初始化5个对象
		for (int i = 0; i < 5; i++) {
			heros.add(new Hero("hero " + i));
		}
		Hero specialHero = new Hero("special hero");
		heros.add(specialHero);
		
		//获取指定位置的对象
		System.out.println(heros.get(5));
		//如果超出了范围,依然会报错
		System.out.println(heros.get(6));

	}

}

 获取对象所处的位置

indexOf用于判断一个对象在ArrayList中所处的位置
contains一样,判断标准是对象是否相同,而非对象的name值是否相等

package collection;

import java.util.ArrayList;

import charactor.Hero;

public class TestCollection {
	public static void main(String[] args) {
		ArrayList heros = new ArrayList();

		// 初始化5个对象
		for (int i = 0; i < 5; i++) {
			heros.add(new Hero("hero " + i));
		}
		Hero specialHero = new Hero("special hero");
		heros.add(specialHero);

		System.out.println(heros);
		System.out.println("specialHero所处的位置:"+heros.indexOf(specialHero));
		System.out.println("新的英雄,但是名字是\"hero 1\"所处的位置:"+heros.indexOf(new Hero("hero 1")));

	}
}

删除

remove用于把对象从ArrayList中删除
remove可以根据下标删除ArrayList的元素

heros.remove(2);


也可以根据对象删除

heros.remove(specialHero);

package collection;

import java.util.ArrayList;

import charactor.Hero;

public class TestCollection {
	public static void main(String[] args) {
		ArrayList heros = new ArrayList();

		// 初始化5个对象
		for (int i = 0; i < 5; i++) {
			heros.add(new Hero("hero " + i));
		}
		Hero specialHero = new Hero("special hero");
		heros.add(specialHero);
		
		System.out.println(heros);
		heros.remove(2);
		System.out.println("删除下标是2的对象");
		System.out.println(heros);
		System.out.println("删除special hero");
		heros.remove(specialHero);
		System.out.println(heros);
		
	}
}

 替换

set用于替换指定位置的元素

package collection;

import java.util.ArrayList;

import charactor.Hero;

public class TestCollection {
	public static void main(String[] args) {
		ArrayList heros = new ArrayList();

		// 初始化5个对象
		for (int i = 0; i < 5; i++) {
			heros.add(new Hero("hero " + i));
		}
		Hero specialHero = new Hero("special hero");
		heros.add(specialHero);
		
		System.out.println(heros);
		System.out.println("把下标是5的元素,替换为\"hero 5\"");
		heros.set(5, new Hero("hero 5"));
		System.out.println(heros);
	}
}

 获取大小

size 用于获取ArrayList的大小

package collection;

import java.util.ArrayList;

import charactor.Hero;

public class TestCollection {
	public static void main(String[] args) {
		ArrayList heros = new ArrayList();

		// 初始化5个对象
		for (int i = 0; i < 5; i++) {
			heros.add(new Hero("hero " + i));
		}
		Hero specialHero = new Hero("special hero");
		heros.add(specialHero);
		System.out.println(heros);
		System.out.println("获取ArrayList的大小:");
		System.out.println(heros.size());
	}
}

 转换为数组

toArray可以把一个ArrayList对象转换为数组。
需要注意的是,如果要转换为一个Hero数组,那么需要传递一个Hero数组类型的对象给toArray(),这样toArray方法才知道,你希望转换为哪种类型的数组,否则只能转换为Object数组

package collection;

import java.util.ArrayList;

import charactor.Hero;

public class TestCollection {
	public static void main(String[] args) {
		ArrayList heros = new ArrayList();

		// 初始化5个对象
		for (int i = 0; i < 5; i++) {
			heros.add(new Hero("hero " + i));
		}
		Hero specialHero = new Hero("special hero");
		heros.add(specialHero);
		System.out.println(heros);
		Hero hs[] = (Hero[])heros.toArray(new Hero[]{});
		System.out.println("数组:" +hs);

	}
}

 把另一个容器所有对象都加进来

addAll 把另一个容器所有对象都加进来

package collection;

import java.util.ArrayList;

import charactor.Hero;

public class TestCollection {
	public static void main(String[] args) {
		ArrayList heros = new ArrayList();

		// 初始化5个对象
		for (int i = 0; i < 5; i++) {
			heros.add(new Hero("hero " + i));
		}

		System.out.println("ArrayList heros:\t" + heros);
		 
		//把另一个容器里所有的元素,都加入到该容器里来
		ArrayList anotherHeros = new ArrayList();
		anotherHeros.add(new Hero("hero a"));
		anotherHeros.add(new Hero("hero b"));
		anotherHeros.add(new Hero("hero c"));
		System.out.println("anotherHeros heros:\t" + anotherHeros);
		heros.addAll(anotherHeros);
		System.out.println("把另一个ArrayList的元素都加入到当前ArrayList:");
		System.out.println("ArrayList heros:\t" + heros);
		
	}
}

清空

clear 清空一个ArrayList

package collection;

import java.util.ArrayList;

import charactor.Hero;

public class TestCollection {
	public static void main(String[] args) {
		ArrayList heros = new ArrayList();

		// 初始化5个对象
		for (int i = 0; i < 5; i++) {
			heros.add(new Hero("hero " + i));
		}

		System.out.println("ArrayList heros:\t" + heros);
		System.out.println("使用clear清空");
		heros.clear();
		System.out.println("ArrayList heros:\t" + heros);
		 
	}
}

 List接口

ArrayList和List

ArrayList实现了接口List
常见的写法会把引用声明为接口List类型
注意:是java.util.List,而不是java.awt.List

package collection;
 
import java.util.ArrayList;
import java.util.List;

import charactor.Hero;
 
public class TestCollection {

    public static void main(String[] args) {
    	//ArrayList实现了接口List
    	
    	//常见的写法会把引用声明为接口List类型
    	//注意:是java.util.List,而不是java.awt.List
    	//接口引用指向子类对象(多态)
    	
        List heros = new ArrayList();
        heros.add( new Hero("盖伦"));
        System.out.println(heros.size());
        
    }
     
}

List接口的方法

因为ArrayList实现了List接口,所以List接口的方法ArrayList都实现了。
ArrayList 常用方法章节有详细的讲解,在此不作赘述

泛型Generic

泛型Generic

不指定泛型的容器,可以存放任何类型的元素
指定了泛型的容器,只能存放指定类型的元素以及其子类

package property;

public class Item {
	String name;
	int price;
	
	public Item(){
		
	}
	
	//提供一个初始化name的构造方法
	public Item(String name){
		this.name = name;
	}
	
	public void effect(){
		System.out.println("物品使用后,可以有效果");
	}
	
}

泛型的简写

为了不使编译器出现警告,需要前后都使用泛型,像这样:

List<Hero> genericheros = new ArrayList<Hero>();


不过JDK7提供了一个可以略微减少代码量的泛型简写方式

List<Hero> genericheros2 = new ArrayList<>();


后面的泛型可以用<>来代替,聊胜于无吧

package collection;
  
import java.util.ArrayList;
import java.util.List;

import charactor.Hero;
  
public class TestCollection {
 
    public static void main(String[] args) {
        List<Hero> genericheros = new ArrayList<Hero>();
        List<Hero> genericheros2 = new ArrayList<>();
     
    }
      
}

泛型的系统学习

泛型的知识还包含 支持泛型的类 泛型转型 通配符 这些内容都在泛型章节详细展开

遍历

用for循环遍历

通过前面的学习,知道了可以用size()和get()分别得到大小,和获取指定位置的元素,结合for循环就可以遍历出ArrayList的内容

package collection;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import charactor.Hero;

public class TestCollection {

	public static void main(String[] args) {
		List<Hero> heros = new ArrayList<Hero>();

		// 放5个Hero进入容器
		for (int i = 0; i < 5; i++) {
			heros.add(new Hero("hero name " + i));
		}

		// 第一种遍历 for循环
		System.out.println("--------for 循环-------");
		for (int i = 0; i < heros.size(); i++) {
			Hero h = heros.get(i);
			System.out.println(h);
		}

	}

}

迭代器遍历

使用迭代器Iterator遍历集合中的元素

package collection;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import charactor.Hero;
 
public class TestCollection {

    public static void main(String[] args) {
    	List<Hero> heros = new ArrayList<Hero>();
    	
    	//放5个Hero进入容器
    	for (int i = 0; i < 5; i++) {
			heros.add(new Hero("hero name " +i));
		}
    	
    	//第二种遍历,使用迭代器
    	System.out.println("--------使用while的iterator-------");
    	Iterator<Hero> it= heros.iterator();
    	//从最开始的位置判断"下一个"位置是否有数据
    	//如果有就通过next取出来,并且把指针向下移动
    	//直到"下一个"位置没有数据
    	while(it.hasNext()){
    		Hero h = it.next();
    		System.out.println(h);
    	}
    	//迭代器的for写法
    	System.out.println("--------使用for的iterator-------");
    	for (Iterator<Hero> iterator = heros.iterator(); iterator.hasNext();) {
			Hero hero = (Hero) iterator.next();
			System.out.println(hero);
		}
    	
    }
     
}

 用增强型for循环

使用增强型for循环可以非常方便的遍历ArrayList中的元素,这是很多开发人员的首选。

不过增强型for循环也有不足:
无法用来进行ArrayList的初始化
无法得知当前是第几个元素了,当需要只打印单数元素的时候,就做不到了。 必须再自定下标变量。

package collection;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import charactor.Hero;

public class TestCollection {

	public static void main(String[] args) {
		List<Hero> heros = new ArrayList<Hero>();

		// 放5个Hero进入容器
		for (int i = 0; i < 5; i++) {
			heros.add(new Hero("hero name " + i));
		}

		// 第三种,增强型for循环
		System.out.println("--------增强型for循环-------");
		for (Hero h : heros) {
			System.out.println(h);
		}

	}

}

2、其他集合

LinkedList

序列分先进先出FIFO,先进后出FILO
FIFO在Java中又叫Queue 队列
FILO在Java中又叫Stack 栈

LinkedList 与 List接口

ArrayList一样,LinkedList也实现了List接口,诸如add,remove,contains等等方法。 详细使用,请参考 ArrayList 常用方法,在此不作赘述。

接下来要讲的是LinkedList的一些特别的地方

双向链表-Deque

除了实现了List接口外,LinkedList还实现了双向链表结构Deque,可以很方便的在头尾插入删除数据

什么是链表结构: 与数组结构相比较,数组结构,就好像是电影院,每个位置都有标示,每个位置之间的间隔都是一样的。 而链表就相当于佛珠,每个珠子,只连接前一个和后一个,不用关心除此之外的其他佛珠在哪里。

package collection;

import java.util.LinkedList;

import charactor.Hero;

public class TestCollection {

    public static void main(String[] args) {
    	
    	//LinkedList是一个双向链表结构的list
    	LinkedList<Hero> ll =new LinkedList<Hero>();
    	
    	//所以可以很方便的在头部和尾部插入数据
    	//在最后插入新的英雄
    	ll.addLast(new Hero("hero1"));
    	ll.addLast(new Hero("hero2"));
    	ll.addLast(new Hero("hero3"));
    	System.out.println(ll);
    	
    	//在最前面插入新的英雄
    	ll.addFirst(new Hero("heroX"));
    	System.out.println(ll);
    	
    	//查看最前面的英雄
    	System.out.println(ll.getFirst());
    	//查看最后面的英雄
    	System.out.println(ll.getLast());
    	
    	//查看不会导致英雄被删除
    	System.out.println(ll);
    	//取出最前面的英雄
    	System.out.println(ll.removeFirst());
    	
    	//取出最后面的英雄
    	System.out.println(ll.removeLast());
    	
    	//取出会导致英雄被删除
    	System.out.println(ll);
    	
    }
     
}

队列- Queue

LinkedList 除了实现了List和Deque外,还实现了Queue接口(队列)。
Queue是先进先出队列 FIFO,常用方法:
offer 在最后添加元素
poll 取出第一个元素
peek 查看第一个元素

package collection;
 
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
 
import charactor.Hero;
 
public class TestCollection {
 
    public static void main(String[] args) {
        //和ArrayList一样,LinkedList也实现了List接口
        List ll =new LinkedList<Hero>();
         
        //所不同的是LinkedList还实现了Deque,进而又实现了Queue这个接口
        //Queue代表FIFO 先进先出的队列
        Queue<Hero> q= new LinkedList<Hero>();
         
        //加在队列的最后面
        System.out.print("初始化队列:\t");
        q.offer(new Hero("Hero1"));
        q.offer(new Hero("Hero2"));
        q.offer(new Hero("Hero3"));
        q.offer(new Hero("Hero4"));
         
        System.out.println(q);
        System.out.print("把第一个元素取poll()出来:\t");
        //取出第一个Hero,FIFO 先进先出
        Hero h = q.poll();
        System.out.println(h);
        System.out.print("取出第一个元素之后的队列:\t");
        System.out.println(q);
         
        //把第一个拿出来看一看,但是不取出来
        h=q.peek();
        System.out.print("查看peek()第一个元素:\t");
        System.out.println(h);
        System.out.print("查看并不会导致第一个元素被取出来:\t");
        System.out.println(q);
         
    }
      
}

ArrayList 与 LinkedList的区别

ArrayList 与 LinkedList的区别是面试常常会问到的考题
具体区别,详见 ArrayList 与 LinkedList的区别

二叉树

二叉树概念

二叉树由各种节点组成
二叉树特点:
每个节点都可以有左子节点,右子节点
每一个节点都有一个值

package collection;

public class Node {
	// 左子节点
	public Node leftNode;
	// 右子节点
	public Node rightNode;
	// 值
	public Object value;
}

 二叉树排序-插入数据

假设通过二叉树对如下10个随机数进行排序
67,7,30,73,10,0,78,81,10,74
排序的第一个步骤是把数据插入到该二叉树中
插入基本逻辑是,小、相同的放左边,大的放右边
1. 67 放在根节点
2. 7 比 67小,放在67的左节点
3. 30 比67 小,找到67的左节点7,30比7大,就放在7的右节点
4. 73 比67大, 放在67的右节点
5. 10 比 67小,找到67的左节点7,10比7大,找到7的右节点30,10比30小,放在30的左节点。
...
...
9. 10比67小,找到67的左节点7,10比7大,找到7的右节点30,10比30小,找到30的左节点10,10和10一样大,放在左边

package collection;
 
public class Node {
    // 左子节点
    public Node leftNode;
    // 右子节点
    public Node rightNode;
 
    // 值
    public Object value;
 
    // 插入 数据
    public void add(Object v) {
        // 如果当前节点没有值,就把数据放在当前节点上
        if (null == value)
            value = v;
 
        // 如果当前节点有值,就进行判断,新增的值与当前值的大小关系
        else {
            // 新增的值,比当前值小或者相同
        	
            if ((Integer) v -((Integer)value) <= 0) {
                if (null == leftNode)
                    leftNode = new Node();
                leftNode.add(v);
            }
            // 新增的值,比当前值大
            else {
                if (null == rightNode)
                    rightNode = new Node();
                rightNode.add(v);
            }
 
        }
 
    }
 
    public static void main(String[] args) {
 
        int randoms[] = new int[] { 67, 7, 30, 73, 10, 0, 78, 81, 10, 74 };
 
        Node roots = new Node();
        for (int number : randoms) {
            roots.add(number);
        }
 
    }
}

二叉树-遍历

通过上一个步骤的插入行为,实际上,数据就已经排好序了。 接下来要做的是看,把这些已经排好序的数据,遍历成我们常用的List或者数组的形式

二叉树的遍历分左序,中序,右序
左序即: 中间的数遍历后放在左边
中序即: 中间的数遍历后放在中间
右序即: 中间的数遍历后放在右边
如图所见,我们希望遍历后的结果是从小到大的,所以应该采用中序遍历

package collection;

import java.util.ArrayList;
import java.util.List;

public class Node {
    // 左子节点
    public Node leftNode;
    // 右子节点
    public Node rightNode;
 
    // 值
    public Object value;
 
    // 插入 数据
    public void add(Object v) {
        // 如果当前节点没有值,就把数据放在当前节点上
        if (null == value)
            value = v;
 
        // 如果当前节点有值,就进行判断,新增的值与当前值的大小关系
        else {
            // 新增的值,比当前值小或者相同
        	
            if ((Integer) v -((Integer)value) <= 0) {
                if (null == leftNode)
                    leftNode = new Node();
                leftNode.add(v);
            }
            // 新增的值,比当前值大
            else {
                if (null == rightNode)
                    rightNode = new Node();
                rightNode.add(v);
            }
 
        }
 
    }
 
 // 中序遍历所有的节点
    public List<Object> values() {
        List<Object> values = new ArrayList<>();
 
        // 左节点的遍历结果
        if (null != leftNode)
            values.addAll(leftNode.values());
 
        // 当前节点
        values.add(value);
 
        // 右节点的遍历结果
        if (null != rightNode)
 
            values.addAll(rightNode.values());
 
        return values;
    }
 
    public static void main(String[] args) {
 
        int randoms[] = new int[] { 67, 7, 30, 73, 10, 0, 78, 81, 10, 74 };
 
        Node roots = new Node();
        for (int number : randoms) {
            roots.add(number);
        }
 
        System.out.println(roots.values());
 
    }
}

 HashMap

HashMap的键值对

HashMap储存数据的方式是—— 键值对

package collection;
  
import java.util.HashMap;
  
public class TestCollection {
    public static void main(String[] args) {
        HashMap<String,String> dictionary = new HashMap<>();
        dictionary.put("adc", "物理英雄");
        dictionary.put("apc", "魔法英雄");
        dictionary.put("t", "坦克");
        
        System.out.println(dictionary.get("t"));
    }
}

键不能重复,值可以重复

对于HashMap而言,key是唯一的,不可以重复的。
所以,以相同的key 把不同的value插入到 Map中会导致旧元素被覆盖,只留下最后插入的元素。
不过,同一个对象可以作为值插入到map中,只要对应的key不一样

package collection;
 
import java.util.HashMap;
 
import charactor.Hero;
 
public class TestCollection {
    public static void main(String[] args) {
        HashMap<String,Hero> heroMap = new HashMap<String,Hero>();
        
        heroMap.put("gareen", new Hero("gareen1"));
        System.out.println(heroMap);
        
        //key为gareen已经有value了,再以gareen作为key放入数据,会导致原英雄,被覆盖
        //不会增加新的元素到Map中
        heroMap.put("gareen", new Hero("gareen2"));
        System.out.println(heroMap);
        
        //清空map
        heroMap.clear();
        Hero gareen = new Hero("gareen");
        
        //同一个对象可以作为值插入到map中,只要对应的key不一样
        heroMap.put("hero1", gareen);
        heroMap.put("hero2", gareen);
        
        System.out.println(heroMap);
        
    }
}

HashSet

元素不能重复

Set中的元素,不能重复

package collection;
 
import java.util.HashSet;
 
public class TestCollection {
    public static void main(String[] args) {
        
    	HashSet<String> names = new HashSet<String>();
    	
    	names.add("gareen");
    	
    	System.out.println(names);
    	
    	//第二次插入同样的数据,是插不进去的,容器中只会保留一个
    	names.add("gareen");
    	System.out.println(names);
    }
}

没有顺序

Set中的元素,没有顺序。
严格的说,是没有按照元素的插入顺序排列

HashSet的具体顺序,既不是按照插入顺序,也不是按照hashcode的顺序。关于hashcode有专门的章节讲解: hashcode 原理

以下是HashSet源代码中的部分注释

/**

* It makes no guarantees as to the iteration order of the set;

* in particular, it does not guarantee that the order will remain constant over time.

*/

不保证Set的迭代顺序; 确切的说,在不同条件下,元素的顺序都有可能不一样



换句话说,同样是插入0-9到HashSet中, 在JVM的不同版本中,看到的顺序都是不一样的。 所以在开发的时候,不能依赖于某种臆测的顺序,这个顺序本身是不稳定的

package collection;

import java.util.HashSet;

public class TestCollection {
	public static void main(String[] args) {
		HashSet<Integer> numbers = new HashSet<Integer>();

		numbers.add(9);
		numbers.add(5);
		numbers.add(1);

		// Set中的元素排列,不是按照插入顺序
		System.out.println(numbers);

	}
}

 遍历

Set不提供get()来获取指定位置的元素
所以遍历需要用到迭代器,或者增强型for循环

package collection;
 
import java.util.HashSet;
import java.util.Iterator;
 
public class TestCollection {
    public static void main(String[] args) {
    	HashSet<Integer> numbers = new HashSet<Integer>();
    	
    	for (int i = 0; i < 20; i++) {
			numbers.add(i);
		}
    	
    	//Set不提供get方法来获取指定位置的元素
    	//numbers.get(0)
    	
    	//遍历Set可以采用迭代器iterator
    	for (Iterator<Integer> iterator = numbers.iterator(); iterator.hasNext();) {
			Integer i = (Integer) iterator.next();
			System.out.println(i);
		}
    	
    	//或者采用增强型for循环
    	for (Integer i : numbers) {
			System.out.println(i);
		}
    	
    }
}

HashSet和HashMap的关系

通过观察HashSet的源代码(如何查看源代码
可以发现HashSet自身并没有独立的实现,而是在里面封装了一个Map.
HashSet是作为Map的key而存在的
而value是一个命名为PRESENT的static的Object对象,因为是一个类属性,所以只会有一个。

private static final Object PRESENT = new Object();

package collection;

import java.util.AbstractSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    //HashSet里封装了一个HashMap
	private  HashMap<E,Object> map;

    private static final Object PRESENT = new Object();

    //HashSet的构造方法初始化这个HashMap
    public HashSet() {
    	map = new HashMap<E,Object>();
    }

    //向HashSet中增加元素,其实就是把该元素作为key,增加到Map中
    //value是PRESENT,静态,final的对象,所有的HashSet都使用这么同一个对象
    public boolean add(E e) {
    	return map.put(e, PRESENT)==null;
    }

    //HashSet的size就是map的size
    public int size() {
    	return map.size();
    }

    //清空Set就是清空Map
    public void clear() {
    	map.clear();
    }
    
    //迭代Set,就是把Map的键拿出来迭代
    public Iterator<E> iterator() {
    	return map.keySet().iterator();
    }

}

Collection

Collection是一个接口

Collection

Collection是 Set List Queue和 Deque的接口
Queue: 先进先出队列
Deque: 双向链表

注:Collection和Map之间没有关系,Collection是放一个一个对象的,Map 是放键值对的
注:Deque 继承 Queue,间接的继承了 Collection

 Collections

Collections是一个类,容器的工具类,就如同Arrays是数组的工具类

反转

reverse 使List中的数据发生翻转

package collection;
  
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
  
public class TestCollection {
    public static void main(String[] args) {
        //初始化集合numbers
        List<Integer> numbers = new ArrayList<>();
        
        for (int i = 0; i < 10; i++) {
            numbers.add(i);
        }
        
        System.out.println("集合中的数据:");
        System.out.println(numbers);
        
        Collections.reverse(numbers);
        
        System.out.println("翻转后集合中的数据:");
        System.out.println(numbers);
        
    }
}

 混淆

shuffle 混淆List中数据的顺序

package collection;
  
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
  
public class TestCollection {
    public static void main(String[] args) {
        //初始化集合numbers
        List<Integer> numbers = new ArrayList<>();
        
        for (int i = 0; i < 10; i++) {
            numbers.add(i);
        }
        
        System.out.println("集合中的数据:");
        System.out.println(numbers);
        
        Collections.shuffle(numbers);
        
        System.out.println("混淆后集合中的数据:");
        System.out.println(numbers);
        
    }
}

排序

sort 对List中的数据进行排序

package collection;
  
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
  
public class TestCollection {
    public static void main(String[] args) {
        //初始化集合numbers
        List<Integer> numbers = new ArrayList<>();
        
        for (int i = 0; i < 10; i++) {
            numbers.add(i);
        }
        
        System.out.println("集合中的数据:");
        System.out.println(numbers);

        Collections.shuffle(numbers);
        System.out.println("混淆后集合中的数据:");
        System.out.println(numbers);

        Collections.sort(numbers);
        System.out.println("排序后集合中的数据:");
        System.out.println(numbers);
        
    }
}

交换

swap 交换两个数据的位置

package collection;
  
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
  
public class TestCollection {
    public static void main(String[] args) {
        //初始化集合numbers
        List<Integer> numbers = new ArrayList<>();
        
        for (int i = 0; i < 10; i++) {
            numbers.add(i);
        }
        
        System.out.println("集合中的数据:");
        System.out.println(numbers);

        Collections.swap(numbers,0,5);
        System.out.println("交换0和5下标的数据后,集合中的数据:");
        System.out.println(numbers);
        
    }
}

滚动

rotate 把List中的数据,向右滚动指定单位的长度

package collection;
  
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
  
public class TestCollection {
    public static void main(String[] args) {
        //初始化集合numbers
        List<Integer> numbers = new ArrayList<>();
        
        for (int i = 0; i < 10; i++) {
            numbers.add(i);
        }
        
        System.out.println("集合中的数据:");
        System.out.println(numbers);

        Collections.rotate(numbers,2);
        System.out.println("把集合向右滚动2个单位,标的数据后,集合中的数据:");
        System.out.println(numbers);
        
    }
}

线程安全化

synchronizedList 把非线程安全的List转换为线程安全的List。 因为截至目前为止,还没有学习线程安全的内容,暂时不展开。 线程安全的内容将在多线程章节展开。

package collection;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class TestCollection {
	public static void main(String[] args) {
		List<Integer> numbers = new ArrayList<>();

		System.out.println("把非线程安全的List转换为线程安全的List");
        List<Integer> synchronizedNumbers = (List<Integer>) Collections.synchronizedList(numbers);

	}
}

3、关系与区别

ArrayList vs HashSet

是否有顺序

ArrayList: 有顺序
HashSet: 无顺序

HashSet的具体顺序,既不是按照插入顺序,也不是按照hashcode的顺序。关于hashcode有专门的章节讲解: hashcode 原理

以下是HasetSet源代码中的部分注释

/**

* It makes no guarantees as to the iteration order of the set;

* in particular, it does not guarantee that the order will remain constant over time.

*/

不保证Set的迭代顺序; 确切的说,在不同条件下,元素的顺序都有可能不一样

换句话说,同样是插入0-9到HashSet中, 在JVM的不同版本中,看到的顺序都是不一样的。 所以在开发的时候,不能依赖于某种臆测的顺序,这个顺序本身是不稳定的

package collection;
  
import java.util.ArrayList;
import java.util.HashSet;
   
public class TestCollection {
    public static void main(String[] args) {
          
        ArrayList<Integer> numberList =new ArrayList<Integer>();
        //List中的数据按照插入顺序存放
        System.out.println("----------List----------");
        System.out.println("向List 中插入 9 5 1");
        numberList.add(9);
        numberList.add(5);
        numberList.add(1);
        System.out.println("List 按照顺序存放数据:");
        System.out.println(numberList);
        System.out.println("----------Set----------");
        HashSet<Integer> numberSet =new HashSet<Integer>();
        System.out.println("向Set 中插入9 5 1");
        //Set中的数据不是按照插入顺序存放
        numberSet.add(9);
        numberSet.add(5);
        numberSet.add(1);
        System.out.println("Set 不是按照顺序存放数据:");
        System.out.println(numberSet);
          
    }
}

 能否重复

List中的数据可以重复
Set中的数据不能够重复
重复判断标准是:
首先看hashcode是否相同
如果hashcode不同,则认为是不同数据
如果hashcode相同,再比较equals,如果equals相同,则是相同数据,否则是不同数据
更多关系hashcode,请参考

hashcode原理

package collection;
  
import java.util.ArrayList;
import java.util.HashSet;
   
public class TestCollection {
    public static void main(String[] args) {
          
        ArrayList<Integer> numberList =new ArrayList<Integer>();
        //List中的数据可以重复
        System.out.println("----------List----------");
        System.out.println("向List 中插入 9 9");
        numberList.add(9);
        numberList.add(9);
        System.out.println("List 中出现两个9:");
        System.out.println(numberList);
        System.out.println("----------Set----------");
        HashSet<Integer> numberSet =new HashSet<Integer>();
        System.out.println("向Set 中插入9 9");
        //Set中的数据不能重复
        numberSet.add(9);
        numberSet.add(9);
        System.out.println("Set 中只会保留一个9:");
        System.out.println(numberSet);
          
    }
}

 ArrayList vs LinkedList

ArrayList 和 LinkedList的区别

ArrayList 插入,删除数据慢
LinkedList, 插入,删除数据快
ArrayList是顺序结构,所以定位很快,指哪找哪。 就像电影院位置一样,有了电影票,一下就找到位置了。
LinkedList 是链表结构,就像手里的一串佛珠,要找出第99个佛珠,必须得一个一个的数过去,所以定位慢

插入数据

package collection;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class TestCollection {
	public static void main(String[] args) {
		List<Integer> l;
		l = new ArrayList<>();
		insertFirst(l, "ArrayList");

		l = new LinkedList<>();
		insertFirst(l, "LinkedList");

	}

	private static void insertFirst(List<Integer> l, String type) {
		int total = 1000 * 100;
		final int number = 5;
		long start = System.currentTimeMillis();
		for (int i = 0; i < total; i++) {
			l.add(0, number);
		}
		long end = System.currentTimeMillis();
		System.out.printf("在%s 最前面插入%d条数据,总共耗时 %d 毫秒 %n", type, total, end - start);
	}

}

定位数据

package collection;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class TestCollection {
	public static void main(String[] args) {
		List<Integer> l;
		l = new ArrayList<>();
		modify(l, "ArrayList");

		l = new LinkedList<>();
		modify(l, "LinkedList");

	}

	private static void modify(List<Integer> l, String type) {
		int total = 100 * 1000;
		int index = total/2;
		final int number = 5;
		//初始化
		for (int i = 0; i < total; i++) {
			l.add(number);
		}
		
		long start = System.currentTimeMillis();

		for (int i = 0; i < total; i++) {
			 int n = l.get(index);
			 n++;
			 l.set(index, n);
		}
		long end = System.currentTimeMillis();
		System.out.printf("%s总长度是%d,定位到第%d个数据,取出来,加1,再放回去%n 重复%d遍,总共耗时 %d 毫秒 %n", type,total, index,total, end - start);
		System.out.println();
	}

}

 HashMap vs HashTable

HashMap 和 HashTable的区别

HashMap和Hashtable都实现了Map接口,都是键值对保存数据的方式
区别1:
HashMap可以存放 null
Hashtable不能存放null
区别2:
HashMap不是线程安全的类
Hashtable是线程安全的类

鉴于目前学习的进度,不对线程安全做展开,在线程章节会详细讲解

package collection;

import java.util.HashMap;
import java.util.Hashtable;

public class TestCollection {
	public static void main(String[] args) {
		
		//HashMap和Hashtable都实现了Map接口,都是键值对保存数据的方式
		
		HashMap<String,String> hashMap = new HashMap<String,String>();
		
		//HashMap可以用null作key,作value
		hashMap.put(null, "123");
		hashMap.put("123", null);
		
		Hashtable<String,String> hashtable = new Hashtable<String,String>();
		//Hashtable不能用null作key,不能用null作value
		hashtable.put(null, "123");
		hashtable.put("123", null);

	}
}

几种Set

HashSet LinkedHashSet TreeSet

HashSet: 无序
LinkedHashSet: 按照插入顺序
TreeSet: 从小到大排序

package collection;
 
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.TreeSet;
 
public class TestCollection {
    public static void main(String[] args) {
        HashSet<Integer> numberSet1 =new HashSet<Integer>();
        //HashSet中的数据不是按照插入顺序存放
        numberSet1.add(88);
        numberSet1.add(8);
        numberSet1.add(888);
         
        System.out.println(numberSet1);
         
        LinkedHashSet<Integer> numberSet2 =new LinkedHashSet<Integer>();
        //LinkedHashSet中的数据是按照插入顺序存放
        numberSet2.add(88);
        numberSet2.add(8);
        numberSet2.add(888);
         
        System.out.println(numberSet2);
        TreeSet<Integer> numberSet3 =new TreeSet<Integer>();
        //TreeSet 中的数据是进行了排序的
        numberSet3.add(88);
        numberSet3.add(8);
        numberSet3.add(888);
         
        System.out.println(numberSet3);
         
    }
}

4、其他

hashcode原理

List查找的低效率

假设在List中存放着无重复名称,没有顺序的2000000个Hero
要把名字叫做“hero 1000000”的对象找出来
List的做法是对每一个进行挨个遍历,直到找到名字叫做“hero 1000000”的英雄。
最差的情况下,需要遍历和比较2000000次,才能找到对应的英雄。
测试逻辑:
1. 初始化2000000个对象到ArrayList中
2. 打乱容器中的数据顺序
3. 进行10次查询,统计每一次消耗的时间
不同计算机的配置情况下,所花的时间是有区别的。 在本机上,花掉的时间大概是600毫秒左右

package collection;
    
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
    
import charactor.Hero;
    
public class TestCollection {
    public static void main(String[] args) {
        List<Hero> heros = new ArrayList<Hero>();
           
        for (int j = 0; j < 2000000; j++) {
            Hero h = new Hero("Hero " + j);
            heros.add(h);
        }
           
        // 进行10次查找,观察大体的平均值
        for (int i = 0; i < 10; i++) {
            // 打乱heros中元素的顺序
            Collections.shuffle(heros);
            
            long start = System.currentTimeMillis();
    
            String target = "Hero 1000000";
    
            for (Hero hero : heros) {
                if (hero.name.equals(target)) {
                    System.out.println("找到了 hero!" );
                    break;
                }
            }
            long end = System.currentTimeMillis();
            long elapsed = end - start;
            System.out.println("一共花了:" + elapsed + " 毫秒");
        }
            
    }
}

 HashMap的性能表现

使用HashMap 做同样的查找
1. 初始化2000000个对象到HashMap中。
2. 进行10次查询
3. 统计每一次的查询消耗的时间
可以观察到,几乎不花时间,花费的时间在1毫秒以内

package collection;
 
import java.util.HashMap;
 
import charactor.Hero;
 
public class TestCollection {
    public static void main(String[] args) {
         
        HashMap<String,Hero> heroMap = new HashMap<String,Hero>();
        for (int j = 0; j < 2000000; j++) {
            Hero h = new Hero("Hero " + j);
            heroMap.put(h.name, h);
        }
        System.out.println("数据准备完成");
 
        for (int i = 0; i < 10; i++) {
            long start = System.currentTimeMillis();
             
            //查找名字是Hero 1000000的对象
            Hero target = heroMap.get("Hero 1000000");
            System.out.println("找到了 hero!" + target.name);
             
            long end = System.currentTimeMillis();
            long elapsed = end - start;
            System.out.println("一共花了:" + elapsed + " 毫秒");
        }
 
    }
}

 HashMap原理与字典

在展开HashMap原理的讲解之前,首先回忆一下大家初中和高中使用的汉英字典。

比如要找一个单词对应的中文意思,假设单词是Lengendary,首先在目录找到Lengendary在第 555页。

然后,翻到第555页,这页不只一个单词,但是量已经很少了,逐一比较,很快就定位目标单词Lengendary。

555相当于就是Lengendary对应的hashcode

分析HashMap性能卓越的原因

-----hashcode概念-----
所有的对象,都有一个对应的hashcode(散列值)
比如字符串“gareen”对应的是1001 (实际上不是,这里是方便理解,假设的值)
比如字符串“temoo”对应的是1004
比如字符串“db”对应的是1008
比如字符串“annie”对应的也是1008

-----保存数据-----
准备一个数组,其长度是2000,并且设定特殊的hashcode算法,使得所有字符串对应的hashcode,都会落在0-1999之间
要存放名字是"gareen"的英雄,就把该英雄和名称组成一个键值对,存放在数组的1001这个位置上
要存放名字是"temoo"的英雄,就把该英雄存放在数组的1004这个位置上
要存放名字是"db"的英雄,就把该英雄存放在数组的1008这个位置上
要存放名字是"annie"的英雄,然而 "annie"的hashcode 1008对应的位置已经有db英雄了,那么就在这里创建一个链表,接在db英雄后面存放annie

-----查找数据-----
比如要查找gareen,首先计算"gareen"的hashcode是1001,根据1001这个下标,到数组中进行定位,(根据数组下标进行定位,是非常快速的) 发现1001这个位置就只有一个英雄,那么该英雄就是gareen.
比如要查找annie,首先计算"annie"的hashcode是1008,根据1008这个下标,到数组中进行定位,发现1008这个位置有两个英雄,那么就对两个英雄的名字进行逐一比较(equals),因为此时需要比较的量就已经少很多了,很快也就可以找出目标英雄
这就是使用hashmap进行查询,非常快原理。


这是一种用空间换时间的思维方式

HashSet判断是否重复

HashSet的数据是不能重复的,相同数据不能保存在一起,到底如何判断是否是重复的呢?
根据HashSet和HashMap的关系,我们了解到因为HashSet没有自身的实现,而是里面封装了一个HashMap,所以本质上就是判断HashMap的key是否重复。

再通过上一步的学习,key是否重复,是由两个步骤判断的:
hashcode是否一样
如果hashcode不一样,就是在不同的坑里,一定是不重复的
如果hashcode一样,就是在同一个坑里,还需要进行equals比较
如果equals一样,则是重复数据
如果equals不一样,则是不同数据。

比较器

Comparator

假设Hero有三个属性 name,hp,damage
一个集合中放存放10个Hero,通过Collections.sort对这10个进行排序
那么到底是hp小的放前面?还是damage小的放前面?Collections.sort也无法确定
所以要指定到底按照哪种属性进行排序
这里就需要提供一个Comparator给定如何进行两个对象之间的大小比较

package charactor;
 
public class Hero  {
    public String name;
    public float hp;
 
    public int damage;
 
    public Hero() {
 
    }
 
    public Hero(String name) {

        this.name = name;
    }
 
 	public String toString() {
		return "Hero [name=" + name + ", hp=" + hp + ", damage=" + damage + "]\r\n";
	}

	public Hero(String name, int hp, int damage) {
    	this.name = name;
    	this.hp = hp;
    	this.damage = damage;
	}
 
}

 Comparable

使Hero类实现Comparable接口
在类里面提供比较算法
Collections.sort就有足够的信息进行排序了,也无需额外提供比较器Comparator
注: 如果返回-1, 就表示当前的更小,否则就是更大

package charactor;
   
public class Hero implements Comparable<Hero>{
    public String name; 
    public float hp;
      
    public int damage;
      
    public Hero(){
         
    }
     
    public Hero(String name) {
        this.name =name;
 
    }
     
    //初始化name,hp,damage的构造方法
    public Hero(String name,float hp, int damage) {
        this.name =name;
        this.hp = hp;
        this.damage = damage;
    }
 
    @Override
    public int compareTo(Hero anotherHero) {
        if(damage<anotherHero.damage)
            return 1;  
        else
        	return -1;
    }
 
    @Override
    public String toString() {
        return "Hero [name=" + name + ", hp=" + hp + ", damage=" + damage + "]\r\n";
    }
     
}

 聚合操作

JDK8之后,引入了对集合的聚合操作,可以非常容易的遍历,筛选,比较集合中的元素。

像这样:

String name =heros

.stream()

.sorted((h1,h2)->h1.hp>h2.hp?-1:1)

.skip(2)

.map(h->h.getName())

.findFirst()

.get();

但是要用好聚合,必须先掌握Lambda表达式,聚合的章节讲放在Lambda与聚合操作部分详细讲解

package lambda;
 
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Random;

import charactor.Hero;
 
public class TestAggregate {
 
    public static void main(String[] args) {
        Random r = new Random();
        List<Hero> heros = new ArrayList<Hero>();
        for (int i = 0; i < 10; i++) {
            heros.add(new Hero("hero " + i, r.nextInt(1000), r.nextInt(100)));
        }

        System.out.println("初始化集合后的数据 (最后一个数据重复):");
        System.out.println(heros);
        
        //传统方式
        Collections.sort(heros,new Comparator<Hero>() {
			@Override
			public int compare(Hero o1, Hero o2) {
				return (int) (o2.hp-o1.hp);
			}
		});
        
        Hero hero = heros.get(2);
        System.out.println("通过传统方式找出来的hp第三高的英雄名称是:" + hero.name);
        
        //聚合方式
        String name =heros
        	.stream()
        	.sorted((h1,h2)->h1.hp>h2.hp?-1:1)
        	.skip(2)
        	.map(h->h.getName())
        	.findFirst()
        	.get();

        System.out.println("通过聚合操作找出来的hp第三高的英雄名称是:" + name);
        
    }
}

 1.4 泛型

1、集合中的泛型

不使用泛型

不使用泛型带来的问题
ADHero(物理攻击英雄) APHero(魔法攻击英雄)都是Hero的子类
ArrayList 默认接受Object类型的对象,所以所有对象都可以放进ArrayList中
所以get(0) 返回的类型是Object
接着,需要进行强制转换才可以得到APHero类型或者ADHero类型。
如果软件开发人员记忆比较好,能记得哪个是哪个,还是可以的。 但是开发人员会犯错误,比如第20行,会记错,把第0个对象转换为ADHero,这样就会出现类型转换异常

package generic;

import java.util.ArrayList;

import charactor.ADHero;
import charactor.APHero;

public class TestGeneric {

	public static void main(String[] args) {
		
		ArrayList heros = new ArrayList();
		
		heros.add(new APHero());
		heros.add(new ADHero());
		
		APHero apHero =  (APHero) heros.get(0);
		ADHero adHero =  (ADHero) heros.get(1);
		
		ADHero adHero2 =  (ADHero) heros.get(0);
	}
}

使用泛型

使用泛型的好处:
泛型的用法是在容器后面添加<Type>
Type可以是类,抽象类,接口
泛型表示这种容器,只能存放APHero,ADHero就放不进去了。

package generic;

import java.util.ArrayList;

import charactor.APHero;

public class TestGeneric {

	public static void main(String[] args) {
		ArrayList<APHero> heros = new ArrayList<APHero>();
		
		//只有APHero可以放进去		
		heros.add(new APHero());
		
		//ADHero甚至放不进去
		//heros.add(new ADHero());
		
		//获取的时候也不需要进行转型,因为取出来一定是APHero
		APHero apHero =  heros.get(0);
		
	}
}

子类对象

假设容器的泛型是Hero,那么Hero的子类APHero,ADHero都可以放进去
和Hero无关的类型Item还是放不进去

package generic;

import java.util.ArrayList;

import property.Item;

import charactor.ADHero;
import charactor.APHero;
import charactor.Hero;

public class TestGeneric {

	public static void main(String[] args) {
		ArrayList<Hero> heros = new ArrayList<Hero>();
		
		//只有作为Hero的子类可以放进去		
		heros.add(new APHero());
		heros.add(new ADHero());
		
		//和Hero无关的类型Item还是放不进去
		//heros.add(new Item());
		
	}
}

泛型的简写

为了不使编译器出现警告,需要前后都使用泛型,像这样:

ArrayList<Hero> heros = new ArrayList<Hero>();


不过JDK7提供了一个可以略微减少代码量的泛型简写方式

ArrayList<Hero> heros2 = new ArrayList<>();


后面的泛型可以用<>来代替,聊胜于无吧

package generic;
 
import java.util.ArrayList;

import charactor.Hero;
 
public class TestGeneric {
 
    public static void main(String[] args) {
        ArrayList<Hero> heros = new ArrayList<Hero>();
        //后面可以只用<>
        ArrayList<Hero> heros2 = new ArrayList<>();
        
    }
}

2、支持泛型的类

不支持泛型的Stack

Stack栈为例子,如果不使用泛型
当需要一个只能放Hero的栈的时候,就需要设计一个HeroStack
当需要一个只能放Item的栈的时候,就需要一个ItemStack

package generic;
  
import java.util.LinkedList;

import charactor.Hero;
  
public class HeroStack {
  
    LinkedList<Hero> heros = new LinkedList<Hero>();
      
    public void push(Hero h) {
        heros.addLast(h);
    }
  
    public Hero pull() {
        return heros.removeLast();
    }
  
    public Hero peek() {
        return heros.getLast();
    }
      
    public static void main(String[] args) {
          
        HeroStack heroStack = new HeroStack();
        for (int i = 0; i < 5; i++) {
            Hero h = new Hero("hero name " + i);
            System.out.println("压入 hero:" + h);
            heroStack.push(h);
        }
        for (int i = 0; i < 5; i++) {
            Hero h =heroStack.pull();
            System.out.println("弹出 hero" + h);
        }
    }
  
}

支持泛型的Stack

设计一个支持泛型的栈MyStack
设计这个类的时候,在类的声明上,加上一个<T>,表示该类支持泛型。
T是type的缩写,也可以使用任何其他的合法的变量,比如A,B,X都可以,但是一般约定成俗使用T,代表类型。

package generic;
  
import java.util.HashMap;
import java.util.LinkedList;

import charactor.Hero;
import property.Item;
  
public class MyStack<T> {
  
    LinkedList<T> values = new LinkedList<T>();
      
    public void push(T t) {
        values.addLast(t);
    }
  
    public T pull() {
        return values.removeLast();
    }
  
    public T peek() {
        return values.getLast();
    }
      
    public static void main(String[] args) {
    	//在声明这个Stack的时候,使用泛型<Hero>就表示该Stack只能放Hero
    	MyStack<Hero> heroStack = new MyStack<>();
        heroStack.push(new Hero());
        //不能放Item
        heroStack.push(new Item());
        
    	//在声明这个Stack的时候,使用泛型<Item>就表示该Stack只能放Item
    	MyStack<Item> itemStack = new MyStack<>();
    	itemStack.push(new Item());
        //不能放Hero
    	itemStack.push(new Hero());
    }
  
}

3、通配符

?extends

ArrayList heroList<? extends Hero> 表示这是一个Hero泛型或者其子类泛型
heroList 的泛型可能是Hero
heroList 的泛型可能是APHero
heroList 的泛型可能是ADHero
所以 可以确凿的是,从heroList取出来的对象,一定是可以转型成Hero的

但是,不能往里面放东西,因为
放APHero就不满足<ADHero>
放ADHero又不满足<APHero>

package generic;
  
import java.util.ArrayList;
 
import charactor.ADHero;
import charactor.APHero;
import charactor.Hero;
  
public class TestGeneric {
  
    public static void main(String[] args) {
         
        ArrayList<APHero> apHeroList = new ArrayList<APHero>();
        apHeroList.add(new APHero());
        
        ArrayList<? extends Hero> heroList = apHeroList;
         
        //? extends Hero 表示这是一个Hero泛型的子类泛型
         
        //heroList 的泛型可以是Hero
        //heroList 的泛型可以使APHero
        //heroList 的泛型可以使ADHero
         
        //可以确凿的是,从heroList取出来的对象,一定是可以转型成Hero的
         
        Hero h= heroList.get(0);
         
        //但是,不能往里面放东西
        heroList.add(new ADHero()); //编译错误,因为heroList的泛型 有可能是APHero
         
    }
     
}

?super

ArrayList heroList<? super Hero> 表示这是一个Hero泛型或者其父类泛型
heroList的泛型可能是Hero
heroList的泛型可能是Object

可以往里面插入Hero以及Hero的子类
但是取出来有风险,因为不确定取出来是Hero还是Object

package generic;
 
import java.util.ArrayList;
 
import charactor.ADHero;
import charactor.APHero;
import charactor.Hero;
 
public class TestGeneric {
    public static void main(String[] args) {
 
        ArrayList<? super Hero> heroList = new ArrayList<Object>();
         
        //? super Hero 表示 heroList的泛型是Hero或者其父类泛型
         
        //heroList 的泛型可以是Hero
        //heroList 的泛型可以是Object
         
        //所以就可以插入Hero
        heroList.add(new Hero());
        //也可以插入Hero的子类
        heroList.add(new APHero());
        heroList.add(new ADHero());
         
        //但是,不能从里面取数据出来,因为其泛型可能是Object,而Object是强转Hero会失败
        Hero h= heroList.get(0);
         
    }
 
}

 泛型通配符?

泛型通配符? 代表任意泛型
既然?代表任意泛型,那么换句话说,这个容器什么泛型都有可能

所以只能以Object的形式取出来
并且不能往里面放对象,因为不知道到底是一个什么泛型的容器

package generic;
 
import java.util.ArrayList;

import property.Item;
import charactor.APHero;
import charactor.Hero;
 
public class TestGeneric {
 
    public static void main(String[] args) {
 
        ArrayList<APHero> apHeroList = new ArrayList<APHero>();
        
        //?泛型通配符,表示任意泛型
        ArrayList<?> generalList = apHeroList;

        //?的缺陷1: 既然?代表任意泛型,那么换句话说,你就不知道这个容器里面是什么类型
        //所以只能以Object的形式取出来
        Object o = generalList.get(0);

        //?的缺陷2: 既然?代表任意泛型,那么既有可能是Hero,也有可能是Item
        //所以,放哪种对象进去,都有风险,结果就什么什么类型的对象,都不能放进去
        generalList.add(new Item()); //编译错误 因为?代表任意泛型,很有可能不是Item
        generalList.add(new Hero()); //编译错误 因为?代表任意泛型,很有可能不是Hero
        generalList.add(new APHero()); //编译错误  因为?代表任意泛型,很有可能不是APHero
 
    }
}

 总结

如果希望只取出,不插入,就使用? extends Hero
如果希望只插入,不取出,就使用? super Hero
如果希望,又能插入,又能取出,就不要用通配符?

4、泛型转型

对象转型

根据面向对象学习的知识,子类转父类 是一定可以成功的

package generic;

import charactor.ADHero;
import charactor.Hero;

public class TestGeneric {

	public static void main(String[] args) {

		Hero h = new Hero();
		ADHero ad = new ADHero();
		//子类转父类
		h = ad;
	}

}

子类泛型转父类泛型

既然 子类对象 转 父类对象是可以成功的,那么子类泛型转父类泛型能成功吗?
如代码
hs的泛型是父类Hero
adhs 的泛型是子类ADHero

那么 把adhs转换为hs能成功吗?

package generic;

import java.util.ArrayList;

import charactor.ADHero;
import charactor.Hero;

public class TestGeneric {

	public static void main(String[] args) {
		ArrayList<Hero> hs =new ArrayList<>();
		ArrayList<ADHero> adhs =new ArrayList<>();

		//子类泛型转父类泛型
		hs = adhs;
	}

}

假设可以转型成功

假设可以转型成功
引用hs指向了ADHero泛型的容器
作为Hero泛型的引用hs, 看上去是可以往里面加一个APHero的。
但是hs这个引用,实际上是指向的一个ADHero泛型的容器
如果能加进去,就变成了ADHero泛型的容器里放进了APHero,这就矛盾了

所以子类泛型不可以转换为父类泛型

package generic;

import java.util.ArrayList;

import charactor.ADHero;
import charactor.APHero;
import charactor.Hero;

public class TestGeneric {

	public static void main(String[] args) {
		ArrayList<Hero> hs =new ArrayList<>();
		ArrayList<ADHero> adhs =new ArrayList<>();

		//假设能转换成功
		hs = adhs;
		
		//作为Hero泛型的hs,是可以向其中加入APHero的
		//但是hs这个引用,实际上是指向的一个ADHero泛型的容器
		//如果能加进去,就变成了ADHero泛型的容器里放进了APHero,这就矛盾了
		hs.add(new APHero());
	}

}

 1.5 LAMBDA

1、Hello Lambda

假设一个情景: 找出满足条件的Hero
本教程将从使用普通方法,匿名类,以及Lambda这几种方式,逐渐的引入Lambda的概念

普通方法

使用一个普通方法,在for循环遍历中进行条件判断,筛选出满足条件的数据

hp>100 && damage<50

package lambda;
 
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
 
import charactor.Hero;
 
public class TestLambda {
    public static void main(String[] args) {
        Random r = new Random();
        List<Hero> heros = new ArrayList<Hero>();
        for (int i = 0; i < 10; i++) {
            heros.add(new Hero("hero " + i, r.nextInt(1000), r.nextInt(100)));
        }
        System.out.println("初始化后的集合:");
        System.out.println(heros);
        System.out.println("筛选出 hp>100 && damange<50的英雄");
        filter(heros);
    }
 
    private static void filter(List<Hero> heros) {
        for (Hero hero : heros) {
            if(hero.hp>100 && hero.damage<50)
                System.out.print(hero);
        }
    }
 
}

匿名类方式

首先准备一个接口HeroChecker,提供一个test(Hero)方法
然后通过匿名类的方式,实现这个接口

HeroChecker checker = new HeroChecker() {

public boolean test(Hero h) {

return (h.hp>100 && h.damage<50);

}

};


接着调用filter,传递这个checker进去进行判断,这种方式就很像通过Collections.sort在对一个Hero集合排序,需要传一个Comparator的匿名类对象进去一样。

package lambda;
  
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
  
import charactor.Hero;
  
public class TestLambda {
    public static void main(String[] args) {
        Random r = new Random();
        List<Hero> heros = new ArrayList<Hero>();
        for (int i = 0; i < 5; i++) {
            heros.add(new Hero("hero " + i, r.nextInt(1000), r.nextInt(100)));
        }
        System.out.println("初始化后的集合:");
        System.out.println(heros);
        System.out.println("使用匿名类的方式,筛选出 hp>100 && damange<50的英雄");
        HeroChecker checker = new HeroChecker() {
            @Override
            public boolean test(Hero h) {
                return (h.hp>100 && h.damage<50);
            }
        };
          
        filter(heros,checker);
    }
  
    private static void filter(List<Hero> heros,HeroChecker checker) {
        for (Hero hero : heros) {
            if(checker.test(hero))
                System.out.print(hero);
        }
    }
  
}

 Lambda方式

使用Lambda方式筛选出数据

filter(heros,(h)->h.hp>100 && h.damage<50);


同样是调用filter方法,从上一步的传递匿名类对象,变成了传递一个Lambda表达式进去

h->h.hp>100 && h.damage<50



咋一看Lambda表达式似乎不好理解,其实很简单,下一步讲解如何从一个匿名类一点点演变成Lambda表达式

package lambda;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import charactor.Hero;

public class TestLamdba {
	public static void main(String[] args) {
		Random r = new Random();
		List<Hero> heros = new ArrayList<Hero>();
		for (int i = 0; i < 5; i++) {
			heros.add(new Hero("hero " + i, r.nextInt(1000), r.nextInt(100)));
		}
		System.out.println("初始化后的集合:");
		System.out.println(heros);
		System.out.println("使用Lamdba的方式,筛选出 hp>100 && damange<50的英雄");
		filter(heros,h->h.hp>100 && h.damage<50);
	}

	private static void filter(List<Hero> heros,HeroChecker checker) {
		for (Hero hero : heros) {
			if(checker.test(hero))
				System.out.print(hero);
		}
	}

}

 设置eclipse以支持Lambda

如果你的eclipse能够正常识别Lambda,那么就可以跳过这个章节了。
因为Lambda是JDK8的内容,除了JDK需要使用1.8以上版本外(在JDK环境变量配置下载的就是1.8了),还需要在eclipse中把编译器设置为1.8才能够正常识别Lambda.

设置办法:
菜单->Window->Preferences->Java-Compiler->Compiler compliance leve: 设置为1.8即可

从匿名类演变成Lambda表达式

Lambda表达式可以看成是匿名类一点点演变过来
1. 匿名类的正常写法

HeroChecker c1 = new HeroChecker() {

public boolean test(Hero h) {

return (h.hp>100 && h.damage<50);

}

};


2. 把外面的壳子去掉
只保留方法参数和方法体
参数和方法体之间加上符号 ->

HeroChecker c2 = (Hero h) ->{

return h.hp>100 && h.damage<50;

};

3. 把return和{}去掉

HeroChecker c3 = (Hero h) ->h.hp>100 && h.damage<50;

4. 把 参数类型和圆括号去掉(只有一个参数的时候,才可以去掉圆括号)

HeroChecker c4 = h ->h.hp>100 && h.damage<50;

5. 把c4作为参数传递进去

filter(heros,c4);

6. 直接把表达式传递进去

filter(heros, h -> h.hp > 100 && h.damage < 50);

package lambda;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import charactor.Hero;

public class TestLamdba {
	public static void main(String[] args) {
		Random r = new Random();
		List<Hero> heros = new ArrayList<Hero>();
		for (int i = 0; i < 5; i++) {
			heros.add(new Hero("hero " + i, r.nextInt(1000), r.nextInt(100)));
		}
		System.out.println("初始化后的集合:");
		System.out.println(heros);
		System.out.println("使用匿名类的方式,筛选出 hp>100 && damange<50的英雄");
		// 匿名类的正常写法
		HeroChecker c1 = new HeroChecker() {
			@Override
			public boolean test(Hero h) {
				return (h.hp > 100 && h.damage < 50);
			}
		};
		// 把new HeroChcekcer,方法名,方法返回类型信息去掉
		// 只保留方法参数和方法体
		// 参数和方法体之间加上符号 ->
		HeroChecker c2 = (Hero h) -> {
			return h.hp > 100 && h.damage < 50;
		};

		// 把return和{}去掉
		HeroChecker c3 = (Hero h) -> h.hp > 100 && h.damage < 50;

		// 把 参数类型和圆括号去掉
		HeroChecker c4 = h -> h.hp > 100 && h.damage < 50;

		// 把c4作为参数传递进去
		filter(heros, c4);
		
		// 直接把表达式传递进去
		filter(heros, h -> h.hp > 100 && h.damage < 50);
	}

	private static void filter(List<Hero> heros, HeroChecker checker) {
		for (Hero hero : heros) {
			if (checker.test(hero))
				System.out.print(hero);
		}
	}

}

匿名方法

匿名类 概念相比较,
Lambda 其实就是匿名方法,这是一种把方法作为参数进行传递的编程思想。

虽然代码是这么写

filter(heros, h -> h.hp > 100 && h.damage < 50);


但是,Java会在背后,悄悄的,把这些都还原成匿名类方式
引入Lambda表达式,会使得代码更加紧凑,而不是各种接口和匿名类到处飞。

Lambda的弊端

Lambda表达式虽然带来了代码的简洁,但是也有其局限性。
1. 可读性差,与啰嗦的但是清晰的匿名类代码结构比较起来,Lambda表达式一旦变得比较长,就难以理解
2. 不便于调试,很难在Lambda表达式中增加调试信息,比如日志
3. 版本支持,Lambda表达式在JDK8版本中才开始支持,如果系统使用的是以前的版本,考虑系统的稳定性等原因,而不愿意升级,那么就无法使用。

Lambda比较适合用在简短的业务代码中,并不适合用在复杂的系统中,会加大维护成本。

2、方法引用

引用静态方法

首先为TestLambda添加一个静态方法:

public static boolean testHero(Hero h) {

return h.hp>100 && h.damage<50;

}
Lambda表达式:

filter(heros, h->h.hp>100 && h.damage<50);
在Lambda表达式中调用这个静态方法:

filter(heros, h -> TestLambda.testHero(h) );
调用静态方法还可以改写为:

filter(heros, TestLambda::testHero);
这种方式就叫做引用静态方法

package lambda;
  
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
  
import charactor.Hero;
  
public class TestLambda {
    public static void main(String[] args) {
        Random r = new Random();
        List<Hero> heros = new ArrayList<Hero>();
        for (int i = 0; i < 5; i++) {
            heros.add(new Hero("hero " + i, r.nextInt(1000), r.nextInt(100)));
        }
        System.out.println("初始化后的集合:");
        System.out.println(heros);
          
        HeroChecker c = new HeroChecker() {
            public boolean test(Hero h) {
                return h.hp>100 && h.damage<50;
            }
        };
         
        System.out.println("使用匿名类过滤");
        filter(heros, c);
        System.out.println("使用Lambda表达式");
        filter(heros, h->h.hp>100 && h.damage<50);
        System.out.println("在Lambda表达式中使用静态方法");
        filter(heros, h -> TestLambda.testHero(h) );
        System.out.println("直接引用静态方法");
        filter(heros, TestLambda::testHero);
    }
      
    public static boolean testHero(Hero h) {
        return h.hp>100 && h.damage<50;
    }
      
    private static void filter(List<Hero> heros, HeroChecker checker) {
        for (Hero hero : heros) {
            if (checker.test(hero))
                System.out.print(hero);
        }
    }
  
}

 引用对象方法

与引用静态方法很类似,只是传递方法的时候,需要一个对象的存在

TestLambda testLambda = new TestLambda();

filter(heros, testLambda::testHero);


这种方式叫做引用对象方法

package lambda;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import charactor.Hero;

public class TestLambda {
	public static void main(String[] args) {
		Random r = new Random();
		List<Hero> heros = new ArrayList<Hero>();
		for (int i = 0; i < 5; i++) {
			heros.add(new Hero("hero " + i, r.nextInt(1000), r.nextInt(100)));
		}
		System.out.println("初始化后的集合:");
		System.out.println(heros);
	
		System.out.println("使用引用对象方法  的过滤结果:");
		//使用类的对象方法
		TestLambda testLambda = new TestLambda();
		filter(heros, testLambda::testHero);
	}
	
	public boolean testHero(Hero h) {
		return h.hp>100 && h.damage<50;
	}
	
	private static void filter(List<Hero> heros, HeroChecker checker) {
		for (Hero hero : heros) {
			if (checker.test(hero))
				System.out.print(hero);
		}
	}

}

 引用容器中的对象方法

首先为Hero添加一个方法

public boolean matched(){

return this.hp>100 && this.damage<50;

}
使用Lambda表达式

filter(heros,h-> h.hp>100 && h.damage<50 );
在Lambda表达式中调用容器中的对象Hero的方法matched

filter(heros,h-> h.matched() );
matched恰好就是容器中的对象Hero的方法,那就可以进一步改写为

filter(heros, Hero::matched);
这种方式就叫做引用容器中的对象的方法

package lambda;
  
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
  
import charactor.Hero;
  
public class TestLambda {
    public static void main(String[] args) {
        Random r = new Random();
        List<Hero> heros = new ArrayList<Hero>();
        for (int i = 0; i < 5; i++) {
            heros.add(new Hero("hero " + i, r.nextInt(1000), r.nextInt(100)));
        }
        System.out.println("初始化后的集合:");
        System.out.println(heros);
        
        System.out.println("Lambda表达式:");        
        filter(heros,h-> h.hp>100 && h.damage<50 );

        System.out.println("Lambda表达式中调用容器中的对象的matched方法:");        
        filter(heros,h-> h.matched() );
 
        System.out.println("引用容器中对象的方法 之过滤结果:");        
        filter(heros, Hero::matched);
    }
      
    public boolean testHero(Hero h) {
        return h.hp>100 && h.damage<50;
    }
      
    private static void filter(List<Hero> heros, HeroChecker checker) {
        for (Hero hero : heros) {
            if (checker.test(hero))
                System.out.print(hero);
        }
    }
  
}

 引用构造器

有的接口中的方法会返回一个对象,比如java.util.function.Supplier提供
了一个get方法,返回一个对象。

public interface Supplier<T> {

T get();

}
设计一个方法,参数是这个接口

public static List getList(Supplier<List> s){

return s.get();

}
为了调用这个方法,有3种方式
第一种匿名类:

Supplier<List> s = new Supplier<List>() {

public List get() {

return new ArrayList();

}

};

List list1 = getList(s);

第二种:Lambda表达式

List list2 = getList(()->new ArrayList());

第三种:引用构造器

List list3 = getList(ArrayList::new);

package lambda;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;

public class TestLambda {
    public static void main(String[] args) {
	Supplier<List> s = new Supplier<List>() {
		public List get() {
			return new ArrayList();
		}
	};

	//匿名类
	List list1 = getList(s);
	
	//Lambda表达式
	List list2 = getList(()->new ArrayList());
	
	//引用构造器
	List list3 = getList(ArrayList::new);

    }
    
    public static List getList(Supplier<List> s){
    	return s.get();
    }
     
}

3、聚合操作

传统方式与聚合操作方式遍历数据

遍历数据的传统方式就是使用for循环,然后条件判断,最后打印出满足条件的数据

for (Hero h : heros) {

if (h.hp > 100 && h.damage < 50)

System.out.println(h.name);

}


使用聚合操作方式,画风就发生了变化:

heros

.stream()

.filter(h -> h.hp > 100 && h.damage < 50)

.forEach(h -> System.out.println(h.name));

package lambda;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import charactor.Hero;

public class TestAggregate {

	public static void main(String[] args) {
		Random r = new Random();
		List<Hero> heros = new ArrayList<Hero>();
		for (int i = 0; i < 5; i++) {
			heros.add(new Hero("hero " + i, r.nextInt(1000), r.nextInt(100)));
		}

		System.out.println("初始化后的集合:");
		System.out.println(heros);
		System.out.println("查询条件:hp>100 && damage<50");
		System.out.println("通过传统操作方式找出满足条件的数据:");

		for (Hero h : heros) {
			if (h.hp > 100 && h.damage < 50)
				System.out.println(h.name);
		}

		System.out.println("通过聚合操作方式找出满足条件的数据:");
		heros
			.stream()
			.filter(h -> h.hp > 100 && h.damage < 50)
			.forEach(h -> System.out.println(h.name));

	}
}

Stream和管道的概念

heros

.stream()

.filter(h -> h.hp > 100 && h.damage < 50)

.forEach(h -> System.out.println(h.name));


要了解聚合操作,首先要建立Stream和管道的概念
Stream 和Collection结构化的数据不一样,Stream是一系列的元素,就像是生产线上的罐头一样,一串串的出来。
管道指的是一系列的聚合操作。

管道又分3个部分
管道源:在这个例子里,源是一个List
中间操作: 每个中间操作,又会返回一个Stream,比如.filter()又返回一个Stream, 中间操作是“懒”操作,并不会真正进行遍历。
结束操作:当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。 结束操作不会返回Stream,但是会返回int、float、String、 Collection或者像forEach,什么都不返回, 结束操作才进行真正的遍历行为,在遍历的时候,才会去进行中间操作的相关判断

注: 这个Stream和I/O章节的InputStream,OutputStream是不一样的概念。

管道源

把Collection切换成管道源很简单,调用stream()就行了。

heros.stream()


但是数组却没有stream()方法,需要使用

Arrays.stream(hs)


或者

Stream.of(hs)

package lambda;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Random;

import charactor.Hero;

public class TestAggregate {

	public static void main(String[] args) {
		Random r = new Random();
		List<Hero> heros = new ArrayList<Hero>();
		for (int i = 0; i < 5; i++) {
			heros.add(new Hero("hero " + i, r.nextInt(1000), r.nextInt(100)));
		}
		//管道源是集合
		heros
		.stream()
		.forEach(h->System.out.println(h.name));
		
		//管道源是数组
		Hero hs[] = heros.toArray(new Hero[heros.size()]);
		Arrays.stream(hs)
		.forEach(h->System.out.println(h.name));
		
	}
}

中间操作

每个中间操作,又会返回一个Stream,比如.filter()又返回一个Stream, 中间操作是“懒”操作,并不会真正进行遍历。
中间操作比较多,主要分两类
对元素进行筛选 和 转换为其他形式的流
对元素进行筛选:
filter 匹配
distinct 去除重复(根据equals判断)
sorted 自然排序
sorted(Comparator<T>) 指定排序
limit 保留
skip 忽略
转换为其他形式的流
mapToDouble 转换为double的流
map 转换为任意类型的流

package charactor;
     
public class Hero implements Comparable<Hero>{
    public String name; 
    public float hp;
        
    public int damage;
        
    public Hero(){
           
    }
    public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public float getHp() {
		return hp;
	}
	public void setHp(float hp) {
		this.hp = hp;
	}
	public int getDamage() {
		return damage;
	}
	public void setDamage(int damage) {
		this.damage = damage;
	}
	public Hero(String name) {
        this.name =name;
    }
    //初始化name,hp,damage的构造方法
    public Hero(String name,float hp, int damage) {
        this.name =name;
        this.hp = hp;
        this.damage = damage;
    }
   
    @Override
    public int compareTo(Hero anotherHero) {
        if(damage<anotherHero.damage)
            return 1;  
        else
            return -1;
    }
   
    @Override
    public String toString() {
        return "Hero [name=" + name + ", hp=" + hp + ", damage=" + damage + "]\r\n";
    }
       
}

 结束操作

当进行结束操作后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。 结束操作不会返回Stream,但是会返回int、float、String、 Collection或者像forEach,什么都不返回,。
结束操作才真正进行遍历行为,前面的中间操作也在这个时候,才真正的执行。
常见结束操作如下:
forEach() 遍历每个元素
toArray() 转换为数组
min(Comparator<T>) 取最小的元素
max(Comparator<T>) 取最大的元素
count() 总数
findFirst() 第一个元素

package lambda;
 
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;

import org.omg.Messaging.SYNC_WITH_TRANSPORT;

import charactor.Hero;
 
public class TestAggregate {
 
    public static void main(String[] args) {
        Random r = new Random();
        List<Hero> heros = new ArrayList<Hero>();
        for (int i = 0; i < 5; i++) {
            heros.add(new Hero("hero " + i, r.nextInt(1000), r.nextInt(100)));
        }
        System.out.println("遍历集合中的每个数据");
        heros
            .stream()
            .forEach(h->System.out.print(h));
        System.out.println("返回一个数组");
        Object[] hs= heros
	        .stream()
	        .toArray();
        System.out.println(Arrays.toString(hs));
        System.out.println("返回伤害最低的那个英雄");
        Hero minDamageHero =
        heros
	        .stream()
	        .min((h1,h2)->h1.damage-h2.damage)
	        .get();
        System.out.print(minDamageHero);
        System.out.println("返回伤害最高的那个英雄");

        Hero mxnDamageHero =
                heros
                .stream()
                .max((h1,h2)->h1.damage-h2.damage)
                .get();
        System.out.print(mxnDamageHero);      
        
        System.out.println("流中数据的总数");
        long count = heros
        		.stream()
        		.count();
        System.out.println(count);

        System.out.println("第一个英雄");
        Hero firstHero =
                heros
                .stream()
                .findFirst()
                .get();
        
        System.out.println(firstHero);
        
    }
}

1.6 多线程

1、启动一个线程

多线程即在同一时间,可以做多件事情。

创建多线程有3种方式,分别是继承线程类,实现Runnable接口,匿名类

线程概念

首先要理解进程(Processor)和线程(Thread)的区别
进程:启动一个LOL.exe就叫一个进程。 接着又启动一个DOTA.exe,这叫两个进程。
线程:线程是在进程内部同时做的事情,比如在LOL里,有很多事情要同时做,比如"盖伦” 击杀“提莫”,同时“赏金猎人”又在击杀“盲僧”,这就是由多线程来实现的。


此处代码演示的是不使用多线程的情况:
只有在盖伦杀掉提莫后,赏金猎人才开始杀盲僧

package charactor;

import java.io.Serializable;
 
public class Hero{
    public String name; 
    public float hp;
    
    public int damage;
    
    public void attackHero(Hero h) {
    	try {
    		//为了表示攻击需要时间,每次攻击暂停1000毫秒
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
    	h.hp-=damage;
    	System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n",name,h.name,h.name,h.hp);
    	
    	if(h.isDead())
    		System.out.println(h.name +"死了!");
    }

	public boolean isDead() {
		return 0>=hp?true:false;
	}

}

 创建多线程-继承线程类

使用多线程,就可以做到盖伦在攻击提莫的同时,赏金猎人也在攻击盲僧
设计一个类KillThread 继承Thread,并且重写run方法
启动线程办法: 实例化一个KillThread对象,并且调用其start方法
就可以观察到 赏金猎人攻击盲僧的同时,盖伦也在攻击提莫

package multiplethread;

import charactor.Hero;

public class KillThread extends Thread{
	
	private Hero h1;
	private Hero h2;

	public KillThread(Hero h1, Hero h2){
		this.h1 = h1;
		this.h2 = h2;
	}

	public void run(){
		while(!h2.isDead()){
			h1.attackHero(h2);
		}
	}
}

 创建多线程-实现Runnable接口

创建类Battle,实现Runnable接口
启动的时候,首先创建一个Battle对象,然后再根据该battle对象创建一个线程对象,并启动

Battle battle1 = new Battle(gareen,teemo);

new Thread(battle1).start();


battle1 对象实现了Runnable接口,所以有run方法,但是直接调用run方法,并不会启动一个新的线程。
必须,借助一个线程对象的start()方法,才会启动一个新的线程。
所以,在创建Thread对象的时候,把battle1作为构造方法的参数传递进去,这个线程启动的时候,就会去执行battle1.run()方法了。

package multiplethread;

import charactor.Hero;

public class Battle implements Runnable{
	
	private Hero h1;
	private Hero h2;

	public Battle(Hero h1, Hero h2){
		this.h1 = h1;
		this.h2 = h2;
	}

	public void run(){
		while(!h2.isDead()){
			h1.attackHero(h2);
		}
	}
}

创建多线程-匿名类

使用匿名类,继承Thread,重写run方法,直接在run方法中写业务代码
匿名类的一个好处是可以很方便的访问外部的局部变量。
前提是外部的局部变量需要被声明为final。(JDK7以后就不需要了)

package multiplethread;
 
import charactor.Hero;
 
public class TestThread {
 
    public static void main(String[] args) {
         
        Hero gareen = new Hero();
        gareen.name = "盖伦";
        gareen.hp = 616;
        gareen.damage = 50;
 
        Hero teemo = new Hero();
        teemo.name = "提莫";
        teemo.hp = 300;
        teemo.damage = 30;
         
        Hero bh = new Hero();
        bh.name = "赏金猎人";
        bh.hp = 500;
        bh.damage = 65;
         
        Hero leesin = new Hero();
        leesin.name = "盲僧";
        leesin.hp = 455;
        leesin.damage = 80;
         
        //匿名类
        Thread t1= new Thread(){
            public void run(){
                //匿名类中用到外部的局部变量teemo,必须把teemo声明为final
            	//但是在JDK7以后,就不是必须加final的了
                while(!teemo.isDead()){
                    gareen.attackHero(teemo);
                }               
            }
        };
        
        t1.start();
         
        Thread t2= new Thread(){
            public void run(){
                while(!leesin.isDead()){
                    bh.attackHero(leesin);
                }               
            }
        };
        t2.start();
        
    }
     
}

创建多线程的三种方式

把上述3种方式再整理一下:

1. 继承Thread类
2. 实现Runnable接口
3. 匿名类的方式

注: 启动线程是start()方法,run()并不能启动一个新的线程

2、常见线程方法

当前线程暂停

Thread.sleep(1000); 表示当前线程暂停1000毫秒 ,其他线程不受影响
Thread.sleep(1000); 会抛出InterruptedException 中断异常,因为当前线程sleep的时候,有可能被停止,这时就会抛出 InterruptedException

package multiplethread;

public class TestThread {

	public static void main(String[] args) {
		
		Thread t1= new Thread(){
			public void run(){
				int seconds =0;
				while(true){
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					System.out.printf("已经玩了LOL %d 秒%n", seconds++);
				}				
			}
		};
		t1.start();
		
	}
	
}

加入到当前线程中

首先解释一下主线程的概念
所有进程,至少会有一个线程即主线程,即main方法开始执行,就会有一个看不见的主线程存在。
在42行执行t.join,即表明在主线程中加入该线程。
主线程会等待该线程结束完毕, 才会往下运行。

package multiplethread;
 
import charactor.Hero;
 
public class TestThread {
 
    public static void main(String[] args) {
         
        final Hero gareen = new Hero();
        gareen.name = "盖伦";
        gareen.hp = 616;
        gareen.damage = 50;
 
        final Hero teemo = new Hero();
        teemo.name = "提莫";
        teemo.hp = 300;
        teemo.damage = 30;
         
        final Hero bh = new Hero();
        bh.name = "赏金猎人";
        bh.hp = 500;
        bh.damage = 65;
         
        final Hero leesin = new Hero();
        leesin.name = "盲僧";
        leesin.hp = 455;
        leesin.damage = 80;
         
        Thread t1= new Thread(){
            public void run(){
                while(!teemo.isDead()){
                    gareen.attackHero(teemo);
                }               
            }
        };
         
        t1.start();

        //代码执行到这里,一直是main线程在运行
        try {
        	//t1线程加入到main线程中来,只有t1线程运行结束,才会继续往下走
			t1.join();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

        Thread t2= new Thread(){
            public void run(){
                while(!leesin.isDead()){
                    bh.attackHero(leesin);
                }               
            }
        };
        //会观察到盖伦把提莫杀掉后,才运行t2线程
        t2.start();
         
    }
     
}

线程优先级

当线程处于竞争关系的时候,优先级高的线程会有更大的几率获得CPU资源
为了演示该效果,要把暂停时间去掉,多条线程各自会尽力去占有CPU资源
同时把英雄的血量增加100倍,攻击减低到1,才有足够的时间观察到优先级的演示
如图可见,线程1的优先级是MAX_PRIORITY,所以它争取到了更多的CPU资源执行代码

package charactor;
 
import java.io.Serializable;
  
public class Hero{
    public String name; 
    public float hp;
     
    public int damage;
     
    public void attackHero(Hero h) {
    	//把暂停时间去掉,多条线程各自会尽力去占有CPU资源
    	//线程的优先级效果才可以看得出来
//        try {
//            
//            Thread.sleep(0);
//        } catch (InterruptedException e) {
//            // TODO Auto-generated catch block
//            e.printStackTrace();
//        }
        h.hp-=damage;
        System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n",name,h.name,h.name,h.hp);
         
        if(h.isDead())
            System.out.println(h.name +"死了!");
    }
 
    public boolean isDead() {
        return 0>=hp?true:false;
    }
 
}

 临时暂停

当前线程,临时暂停,使得其他线程可以有更多的机会占用CPU资源

package multiplethread;
 
import charactor.Hero;
 
public class TestThread {
 
    public static void main(String[] args) {
         
        final Hero gareen = new Hero();
        gareen.name = "盖伦";
        gareen.hp = 61600;
        gareen.damage = 1;
 
        final Hero teemo = new Hero();
        teemo.name = "提莫";
        teemo.hp = 30000;
        teemo.damage = 1;
         
        final Hero bh = new Hero();
        bh.name = "赏金猎人";
        bh.hp = 50000;
        bh.damage = 1;
         
        final Hero leesin = new Hero();
        leesin.name = "盲僧";
        leesin.hp = 45050;
        leesin.damage = 1;
         
        Thread t1= new Thread(){
            public void run(){

                while(!teemo.isDead()){
                    gareen.attackHero(teemo);
                }               
            }
        };
         
        Thread t2= new Thread(){
            public void run(){
                while(!leesin.isDead()){
                	//临时暂停,使得t1可以占用CPU资源
                	Thread.yield();
                	
                    bh.attackHero(leesin);
                }               
            }
        };
        
        t1.setPriority(5);
        t2.setPriority(5);
        t1.start();
        t2.start();
         
    }
     
}

守护线程

守护线程的概念是: 当一个进程里,所有的线程都是守护线程的时候,结束当前进程。

就好像一个公司有销售部,生产部这些和业务挂钩的部门。
除此之外,还有后勤,行政等这些支持部门。

如果一家公司销售部,生产部都解散了,那么只剩下后勤和行政,那么这家公司也可以解散了。

守护线程就相当于那些支持部门,如果一个进程只剩下守护线程,那么进程就会自动结束。

守护线程通常会被用来做日志,性能统计等工作。

package multiplethread;
 
public class TestThread {
 
    public static void main(String[] args) {
         
        Thread t1= new Thread(){
            public void run(){
                int seconds =0;
                
                while(true){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    System.out.printf("已经玩了LOL %d 秒%n", seconds++);
                    
                }               
            }
        };
        t1.setDaemon(true);
        t1.start();
         
    }
     
}

3、同步

多线程的同步问题指的是多个线程同时修改一个数据的时候,可能导致的问题

多线程的问题,又叫Concurrency 问题

演示同步问题

假设盖伦有10000滴血,并且在基地里,同时又被对方多个英雄攻击
就是有多个线程在减少盖伦的hp
同时又有多个线程在恢复盖伦的hp
假设线程的数量是一样的,并且每次改变的值都是1,那么所有线程结束后,盖伦应该还是10000滴血。
但是。。。

注意: 不是每一次运行都会看到错误的数据产生,多运行几次,或者增加运行的次数

package charactor;
 
public class Hero{
    public String name; 
    public float hp;
    
    public int damage;
    
    //回血
    public void recover(){
    	hp=hp+1;
    }
    
    //掉血
    public void hurt(){
    	hp=hp-1;
    }
    
    public void attackHero(Hero h) {
        h.hp-=damage;
        System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n",name,h.name,h.name,h.hp);
        if(h.isDead())
            System.out.println(h.name +"死了!");
    }
 
    public boolean isDead() {
        return 0>=hp?true:false;
    }
 
}

 分析同步问题产生的原因

1. 假设增加线程先进入,得到的hp是10000
2. 进行增加运算
3. 正在做增加运算的时候,还没有来得及修改hp的值,减少线程来了
4. 减少线程得到的hp的值也是10000
5. 减少线程进行减少运算
6. 增加线程运算结束,得到值10001,并把这个值赋予hp
7. 减少线程也运算结束,得到值9999,并把这个值赋予hp
hp,最后的值就是9999
虽然经历了两个线程各自增减了一次,本来期望还是原值10000,但是却得到了一个9999
这个时候的值9999是一个错误的值,在业务上又叫做脏数据

解决思路

总体解决思路是: 在增加线程访问hp期间,其他线程不可以访问hp
1. 增加线程获取到hp的值,并进行运算
2. 在运算期间,减少线程试图来获取hp的值,但是不被允许
3. 增加线程运算结束,并成功修改hp的值为10001
4. 减少线程,在增加线程做完后,才能访问hp的值,即10001
5. 减少线程运算,并得到新的值10000

synchronized同步对象概念

解决上述问题之前,先理解
synchronized关键字的意义
如下代码:

Object someObject =new Object();

synchronized (someObject){

//此处的代码只有占有了someObject后才可以执行

}
synchronized表示当前线程,独占 对象 someObject
当前线程独占 了对象someObject,如果有其他线程试图占有对象someObject,就会等待,直到当前线程释放对someObject的占用。
someObject 又叫同步对象,所有的对象,都可以作为同步对象
为了达到同步的效果,必须使用同一个同步对象

释放同步对象的方式: synchronized 块自然结束,或者有异常抛出

package multiplethread;
 
import java.text.SimpleDateFormat;
import java.util.Date;
  
public class TestThread {
	
	public static String now(){
		return new SimpleDateFormat("HH:mm:ss").format(new Date());
	}
	
    public static void main(String[] args) {
        final Object someObject = new Object();
         
        Thread t1 = new Thread(){
            public void run(){
                try {
                    System.out.println( now()+" t1 线程已经运行");
                    System.out.println( now()+this.getName()+ " 试图占有对象:someObject");
                    synchronized (someObject) {
                         
                        System.out.println( now()+this.getName()+ " 占有对象:someObject");
                        Thread.sleep(5000);
                        System.out.println( now()+this.getName()+ " 释放对象:someObject");
                    }
                    System.out.println(now()+" t1 线程结束");
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        };
        t1.setName(" t1");
        t1.start();
        Thread t2 = new Thread(){
 
            public void run(){
                try {
                    System.out.println( now()+" t2 线程已经运行");
                    System.out.println( now()+this.getName()+ " 试图占有对象:someObject");
                    synchronized (someObject) {
                        System.out.println( now()+this.getName()+ " 占有对象:someObject");
                        Thread.sleep(5000);
                        System.out.println( now()+this.getName()+ " 释放对象:someObject");
                    }
                    System.out.println(now()+" t2 线程结束");
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        };
        t2.setName(" t2");
        t2.start();
    }
      
}

 使用synchronized解决同步问题

所有需要修改hp的地方,有要建立在占有someObject的基础上。
而对象 someObject在同一时间,只能被一个线程占有。 间接地,导致同一时间,hp只能被一个线程修改。

package multiplethread;
  
import java.awt.GradientPaint;

import charactor.Hero;
  
public class TestThread {
  
    public static void main(String[] args) {

    	final Object someObject = new Object();
    	
        final Hero gareen = new Hero();
        gareen.name = "盖伦";
        gareen.hp = 10000;
         
        int n = 10000;
 
        Thread[] addThreads = new Thread[n];
        Thread[] reduceThreads = new Thread[n];
         
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(){
                public void run(){
                	
                	//任何线程要修改hp的值,必须先占用someObject
                	synchronized (someObject) {
                		gareen.recover();
					}
                    
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            };
            t.start();
            addThreads[i] = t;
             
        }
         
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(){
                public void run(){
                	//任何线程要修改hp的值,必须先占用someObject
                	synchronized (someObject) {
                		gareen.hurt();
                	}
                	
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            };
            t.start();
            reduceThreads[i] = t;
        }
         
        for (Thread t : addThreads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        for (Thread t : reduceThreads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
         
        System.out.printf("%d个增加线程和%d个减少线程结束后%n盖伦的血量是 %.0f%n", n,n,gareen.hp);
         
    }
      
}

 使用hero对象作为同步对象

既然任意对象都可以用来作为同步对象,而所有的线程访问的都是同一个hero对象,索性就使用gareen来作为同步对象
进一步的,对于Hero的hurt方法,加上:
synchronized (this) {
}
表示当前对象为同步对象,即也是gareen为同步对象

package multiplethread;
  
import java.awt.GradientPaint;

import charactor.Hero;
  
public class TestThread {
  
    public static void main(String[] args) {

        final Hero gareen = new Hero();
        gareen.name = "盖伦";
        gareen.hp = 10000;
         
        int n = 10000;
 
        Thread[] addThreads = new Thread[n];
        Thread[] reduceThreads = new Thread[n];
         
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(){
                public void run(){
                	
                	//使用gareen作为synchronized
                	synchronized (gareen) {
                		gareen.recover();
					}
                    
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            };
            t.start();
            addThreads[i] = t;
             
        }
         
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(){
                public void run(){
                	//使用gareen作为synchronized
                	//在方法hurt中有synchronized(this)
                	gareen.hurt();
                    
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            };
            t.start();
            reduceThreads[i] = t;
        }
         
        for (Thread t : addThreads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        for (Thread t : reduceThreads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
         
        System.out.printf("%d个增加线程和%d个减少线程结束后%n盖伦的血量是 %.0f%n", n,n,gareen.hp);
         
    }
      
}

在方法前,加上修饰符synchronized

在recover前,直接加上synchronized ,其所对应的同步对象,就是this
和hurt方法达到的效果是一样
外部线程访问gareen的方法,就不需要额外使用synchronized 了

package charactor;
 
public class Hero{
    public String name; 
    public float hp;
    
    public int damage;
    
    //回血
    //直接在方法前加上修饰符synchronized
    //其所对应的同步对象,就是this
    //和hurt方法达到的效果一样
    public synchronized void recover(){
    	hp=hp+1;
    }
    
    //掉血
    public void hurt(){
    	//使用this作为同步对象
    	synchronized (this) {
    		hp=hp-1;	
		}
    }
    
    public void attackHero(Hero h) {
        h.hp-=damage;
        System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n",name,h.name,h.name,h.hp);
        if(h.isDead())
            System.out.println(h.name +"死了!");
    }
 
    public boolean isDead() {
        return 0>=hp?true:false;
    }
 
}

线程安全的类

如果一个类,其方法都是有synchronized修饰的,那么该类就叫做线程安全的类

同一时间,只有一个线程能够进入 这种类的一个实例 的去修改数据,进而保证了这个实例中的数据的安全(不会同时被多线程修改而变成脏数据)

比如StringBuffer和StringBuilder的区别
StringBuffer的方法都是有synchronized修饰的,StringBuffer就叫做线程安全的类
而StringBuilder就不是线程安全的类

4、线程安全的类

常见的线程安全相关的面试题

HashMap和Hashtable的区别

HashMap和Hashtable都实现了Map接口,都是键值对保存数据的方式
区别1:
HashMap可以存放 null
Hashtable不能存放null
区别2:
HashMap不是线程安全的类
Hashtable是线程安全的类

 StringBuffer和StringBuilder的区别

StringBuffer 是线程安全的
StringBuilder 是非线程安全的

所以当进行大量字符串拼接操作的时候,如果是单线程就用StringBuilder会更快些,如果是多线程,就需要用StringBuffer 保证数据的安全性

非线程安全的为什么会比线程安全的 快? 因为不需要同步嘛,省略了些时间

ArrayList和Vector的区别

通过在eclipse中查看源代码可以得知:

ArrayList类的声明:

public class ArrayList<E> extends AbstractList<E>

implements List<E>, RandomAccess, Cloneable, java.io.Serializable

Vector类的声明:

public class Vector<E> extends AbstractList<E>

implements List<E>, RandomAccess, Cloneable, java.io.Serializable

一模一样的~
他们的区别也在于,Vector是线程安全的类,而ArrayList是非线程安全的。

把非线程安全的集合转换成线程安全

ArrayList是非线程安全的,换句话说,多个线程可以同时进入一个ArrayList对象的add方法

借助Collections.synchronizedList,可以把ArrayList转换为线程安全的List。

与此类似的,还有HashSet,LinkedList,HashMap等等非线程安全的类,都通过工具类Collections转换为线程安全的

package multiplethread;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class TestThread {
   
    public static void main(String[] args) {
    	List<Integer> list1 = new ArrayList<>();
    	List<Integer> list2 = Collections.synchronizedList(list1);
    }
       
}

5、死锁

当业务比较复杂,多线程应用里有可能会发生死锁

演示死锁

1. 线程1 首先占有对象1,接着试图占有对象2
2. 线程2 首先占有对象2,接着试图占有对象1
3. 线程1 等待线程2释放对象2
4. 与此同时,线程2等待线程1释放对象1
就会。。。一直等待下去,直到天荒地老,海枯石烂,山无棱 ,天地合。。。

package multiplethread;
  
import charactor.Hero;
   
public class TestThread {
     
    public static void main(String[] args) {
        final Hero ahri = new Hero();
        ahri.name = "九尾妖狐";
        final Hero annie = new Hero();
        annie.name = "安妮";
        
        Thread t1 = new Thread(){
            public void run(){
            	//占有九尾妖狐
            	synchronized (ahri) {
            		System.out.println("t1 已占有九尾妖狐");
					try {
						//停顿1000毫秒,另一个线程有足够的时间占有安妮
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					
					System.out.println("t1 试图占有安妮");
					System.out.println("t1 等待中 。。。。");
					synchronized (annie) {
						System.out.println("do something");
					}
				}	
            	
            }
        };
        t1.start();
        Thread t2 = new Thread(){
        	public void run(){
        		//占有安妮
        		synchronized (annie) {
        			System.out.println("t2 已占有安妮");
        			try {
        				
        				//停顿1000毫秒,另一个线程有足够的时间占有暂用九尾妖狐
        				Thread.sleep(1000);
        			} catch (InterruptedException e) {
        				// TODO Auto-generated catch block
        				e.printStackTrace();
        			}
        			System.out.println("t2 试图占有九尾妖狐");
        			System.out.println("t2 等待中 。。。。");
        			synchronized (ahri) {
        				System.out.println("do something");
        			}
        		}	
        		
        	}
        };
        t2.start();
   }
       
}

 6、交互

线程之间有交互通知的需求,考虑如下情况:
有两个线程,处理同一个英雄。
一个加血,一个减血。

减血的线程,发现血量=1,就停止减血,直到加血的线程为英雄加了血,才可以继续减血

不好的解决方式

故意设计减血线程频率更高,盖伦的血量迟早会到达1
减血线程中使用while循环判断是否是1,如果是1就不停的循环,直到加血线程回复了血量
这是不好的解决方式,因为会大量占用CPU,拖慢性能

package charactor;
  
public class Hero{
    public String name; 
    public float hp;
     
    public int damage;
     
    public synchronized void recover(){
        hp=hp+1;
    }     

    public synchronized void hurt(){
            hp=hp-1;    
    }
     
    public void attackHero(Hero h) {
        h.hp-=damage;
        System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n",name,h.name,h.name,h.hp);
        if(h.isDead())
            System.out.println(h.name +"死了!");
    }
  
    public boolean isDead() {
        return 0>=hp?true:false;
    }
  
}

使用wait和notify进行线程交互

在Hero类中:hurt()减血方法:当hp=1的时候,执行this.wait().
this.wait()表示 让占有this的线程等待,并临时释放占有
进入hurt方法的线程必然是减血线程,this.wait()会让减血线程临时释放对this的占有。 这样加血线程,就有机会进入recover()加血方法了。


recover() 加血方法:增加了血量,执行this.notify();
this.notify() 表示通知那些等待在this的线程,可以苏醒过来了。 等待在this的线程,恰恰就是减血线程。 一旦recover()结束, 加血线程释放了this,减血线程,就可以重新占有this,并执行后面的减血工作。

package charactor;

public class Hero {
	public String name;
	public float hp;

	public int damage;

	public synchronized void recover() {
		hp = hp + 1;
		System.out.printf("%s 回血1点,增加血后,%s的血量是%.0f%n", name, name, hp);
		// 通知那些等待在this对象上的线程,可以醒过来了,如第20行,等待着的减血线程,苏醒过来
		this.notify();
	}

	public synchronized void hurt() {
		if (hp == 1) {
			try {
				// 让占有this的减血线程,暂时释放对this的占有,并等待
				this.wait();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}

		hp = hp - 1;
		System.out.printf("%s 减血1点,减少血后,%s的血量是%.0f%n", name, name, hp);
	}

	public void attackHero(Hero h) {
		h.hp -= damage;
		System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n", name, h.name, h.name, h.hp);
		if (h.isDead())
			System.out.println(h.name + "死了!");
	}

	public boolean isDead() {
		return 0 >= hp ? true : false;
	}

}

 关于wait、notify和notifyAll

留意wait()和notify() 这两个方法是什么对象上的?

public synchronized void hurt() {

。。。

this.wait();

。。。

}

public synchronized void recover() {

。。。

this.notify();

}
这里需要强调的是,wait方法和notify方法,并不是Thread线程上的方法,它们是Object上的方法。
因为所有的Object都可以被用来作为同步对象,所以准确的讲,wait和notify是同步对象上的方法。
wait()的意思是: 让占用了这个同步对象的线程,临时释放当前的占用,并且等待。 所以调用wait是有前提条件的,一定是在synchronized块里,否则就会出错。
notify() 的意思是,通知一个等待在这个同步对象上的线程,你可以苏醒过来了,有机会重新占用当前对象了。
notifyAll() 的意思是,通知所有的等待在这个同步对象上的线程,你们可以苏醒过来了,有机会重新占用当前对象了。

7、线程池

每一个线程的启动和结束都是比较消耗时间和占用资源的。

如果在系统中用到了很多的线程,大量的启动和结束动作会导致系统的性能变卡,响应变慢。

为了解决这个问题,引入线程池这种设计思想。

线程池的模式很像生产者消费者模式,消费的对象是一个一个的能够运行的任务

线程池设计思路

线程池的思路和生产者消费者模型是很接近的。
1. 准备一个任务容器
2. 一次性启动10个 消费者线程
3. 刚开始任务容器是空的,所以线程都wait在上面。
4. 直到一个外部线程往这个任务容器中扔了一个“任务”,就会有一个消费者线程被唤醒notify
5. 这个消费者线程取出“任务”,并且执行这个任务,执行完毕后,继续等待下一次任务的到来。
6. 如果短时间内,有较多的任务加入,那么就会有多个线程被唤醒,去执行这些任务。

在整个过程中,都不需要创建新的线程,而是循环使用这些已经存在的线程

开发一个自定义线程池

这是一个自定义的线程池,虽然不够完善和健壮,但是已经足以说明线程池的工作原理

缓慢的给这个线程池添加任务,会看到有多条线程来执行这些任务。
线程7执行完毕任务后,又回到池子里,下一次任务来的时候,线程7又来执行新的任务。

package multiplethread;
 
import java.util.LinkedList;
 
public class ThreadPool {
 
    // 线程池大小
    int threadPoolSize;
 
    // 任务容器
    LinkedList<Runnable> tasks = new LinkedList<Runnable>();
 
    // 试图消费任务的线程
 
    public ThreadPool() {
        threadPoolSize = 10;
 
        // 启动10个任务消费者线程
        synchronized (tasks) {
            for (int i = 0; i < threadPoolSize; i++) {
                new TaskConsumeThread("任务消费者线程 " + i).start();
            }
        }
    }
 
    public void add(Runnable r) {
        synchronized (tasks) {
            tasks.add(r);
            // 唤醒等待的任务消费者线程
            tasks.notifyAll();
        }
    }
 
    class TaskConsumeThread extends Thread {
        public TaskConsumeThread(String name) {
            super(name);
        }
 
        Runnable task;
 
        public void run() {
            System.out.println("启动: " + this.getName());
            while (true) {
                synchronized (tasks) {
                    while (tasks.isEmpty()) {
                        try {
                            tasks.wait();
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
                    task = tasks.removeLast();
                    // 允许添加任务的线程可以继续添加任务
                    tasks.notifyAll();
 
                }
                System.out.println(this.getName() + " 获取到任务,并执行");
                task.run();
            }
        }
    }
 
}

 测试线程池

创造一个情景,每个任务执行的时间都是1秒
刚开始是间隔1秒钟向线程池中添加任务

然后间隔时间越来越短,执行任务的线程还没有来得及结束,新的任务又来了。
就会观察到线程池里的其他线程被唤醒来执行这些任务

package multiplethread;
 
public class TestThread {
    public static void main(String[] args) {
        ThreadPool pool= new ThreadPool();
        int sleep=1000;
        while(true){
            pool.add(new Runnable(){
                @Override
                public void run() {
                    //System.out.println("执行任务");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            });
            try {
                Thread.sleep(sleep);
                sleep = sleep>100?sleep-100:sleep;
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
             
        }
         
    }
}

 使用Java自带线程池

java提供自带的线程池,而不需要自己去开发一个自定义线程池了。
线程池类ThreadPoolExecutor在包java.util.concurrent下

ThreadPoolExecutor threadPool= new ThreadPoolExecutor(10, 15, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
第一个参数10 表示这个线程池初始化了10个线程在里面工作
第二个参数15 表示如果10个线程不够用了,就会自动增加到最多15个线程
第三个参数60 结合第四个参数TimeUnit.SECONDS,表示经过60秒,多出来的线程还没有接到活儿,就会回收,最后保持池子里就10个
第四个参数TimeUnit.SECONDS 如上
第五个参数 new LinkedBlockingQueue() 用来放任务的集合

execute方法用于添加新的任务

package multiplethread;
  
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
  
public class TestThread {
  
    public static void main(String[] args) throws InterruptedException {
          
        ThreadPoolExecutor threadPool= new ThreadPoolExecutor(10, 15, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
          
        threadPool.execute(new Runnable(){
  
            @Override
            public void run() {
                // TODO Auto-generated method stub
                System.out.println("任务1");
            }
              
        });
  
    }
  
}

8、Lock对象

与synchronized类似的,lock也能够达到同步的效果

回忆synchronized同步的方式

首先回忆一下 synchronized 同步对象的方式

当一个线程占用 synchronized 同步对象,其他线程就不能占用了,直到释放这个同步对象为止

package multiplethread;
  
import java.text.SimpleDateFormat;
import java.util.Date;
   
public class TestThread {
     
    public static String now(){
        return new SimpleDateFormat("HH:mm:ss").format(new Date());
    }
     
    public static void main(String[] args) {
        final Object someObject = new Object();
          
        Thread t1 = new Thread(){
            public void run(){
                try {
                    System.out.println( now()+" t1 线程已经运行");
                    System.out.println( now()+this.getName()+ " 试图占有对象:someObject");
                    synchronized (someObject) {
                          
                        System.out.println( now()+this.getName()+ " 占有对象:someObject");
                        Thread.sleep(5000);
                        System.out.println( now()+this.getName()+ " 释放对象:someObject");
                    }
                    System.out.println(now()+" t1 线程结束");
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        };
        t1.setName(" t1");
        t1.start();
        Thread t2 = new Thread(){
  
            public void run(){
                try {
                    System.out.println( now()+" t2 线程已经运行");
                    System.out.println( now()+this.getName()+ " 试图占有对象:someObject");
                    synchronized (someObject) {
                        System.out.println( now()+this.getName()+ " 占有对象:someObject");
                        Thread.sleep(5000);
                        System.out.println( now()+this.getName()+ " 释放对象:someObject");
                    }
                    System.out.println(now()+" t2 线程结束");
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        };
        t2.setName(" t2");
        t2.start();
    }
       
}

 使用Lock对象实现同步效果

Lock是一个接口,为了使用一个Lock对象,需要用到

Lock lock = new ReentrantLock();
与 synchronized (someObject) 类似的,lock()方法,表示当前线程占用lock对象,一旦占用,其他线程就不能占用了。
与 synchronized 不同的是,一旦synchronized 块结束,就会自动释放对someObject的占用。 lock却必须调用unlock方法进行手动释放,为了保证释放的执行,往往会把unlock() 放在finally中进行。

package multiplethread;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TestThread {

	public static String now() {
		return new SimpleDateFormat("HH:mm:ss").format(new Date());
	}

	public static void log(String msg) {
		System.out.printf("%s %s %s %n", now() , Thread.currentThread().getName() , msg);
	}

	public static void main(String[] args) {
		Lock lock = new ReentrantLock();

		Thread t1 = new Thread() {
			public void run() {
				try {
					log("线程启动");
					log("试图占有对象:lock");

					lock.lock();

					log("占有对象:lock");
					log("进行5秒的业务操作");
					Thread.sleep(5000);

				} catch (InterruptedException e) {
					e.printStackTrace();
				} finally {
					log("释放对象:lock");
					lock.unlock();
				}
				log("线程结束");
			}
		};
		t1.setName("t1");
		t1.start();
		try {
			//先让t1飞2秒
			Thread.sleep(2000);
		} catch (InterruptedException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}
		Thread t2 = new Thread() {

			public void run() {
				try {
					log("线程启动");
					log("试图占有对象:lock");

					lock.lock();

					log("占有对象:lock");
					log("进行5秒的业务操作");
					Thread.sleep(5000);

				} catch (InterruptedException e) {
					e.printStackTrace();
				} finally {
					log("释放对象:lock");
					lock.unlock();
				}
				log("线程结束");
			}
		};
		t2.setName("t2");
		t2.start();
	}

}

 trylock方法

synchronized 是不占用到手不罢休的,会一直试图占用下去。
与 synchronized 的钻牛角尖不一样,Lock接口还提供了一个trylock方法。
trylock会在指定时间范围内试图占用,占成功了,就啪啪啪。 如果时间到了,还占用不成功,扭头就走~

注意: 因为使用trylock有可能成功,有可能失败,所以后面unlock释放锁的时候,需要判断是否占用成功了,如果没占用成功也unlock,就会抛出异常

package multiplethread;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TestThread {

	public static String now() {
		return new SimpleDateFormat("HH:mm:ss").format(new Date());
	}

	public static void log(String msg) {
		System.out.printf("%s %s %s %n", now() , Thread.currentThread().getName() , msg);
	}

	public static void main(String[] args) {
		Lock lock = new ReentrantLock();

		Thread t1 = new Thread() {
			public void run() {
				boolean locked = false;
				try {
					log("线程启动");
					log("试图占有对象:lock");

					locked = lock.tryLock(1,TimeUnit.SECONDS);
					if(locked){
						log("占有对象:lock");
						log("进行5秒的业务操作");
						Thread.sleep(5000);
					}
					else{
						log("经过1秒钟的努力,还没有占有对象,放弃占有");
					}

				} catch (InterruptedException e) {
					e.printStackTrace();
				} finally {
					
					if(locked){
						log("释放对象:lock");
						lock.unlock();
					}
				}
				log("线程结束");
			}
		};
		t1.setName("t1");
		t1.start();
		try {
			//先让t1飞2秒
			Thread.sleep(2000);
		} catch (InterruptedException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}
		Thread t2 = new Thread() {

			public void run() {
				boolean locked = false;
				try {
					log("线程启动");
					log("试图占有对象:lock");

					locked = lock.tryLock(1,TimeUnit.SECONDS);
					if(locked){
						log("占有对象:lock");
						log("进行5秒的业务操作");
						Thread.sleep(5000);
					}
					else{
						log("经过1秒钟的努力,还没有占有对象,放弃占有");
					}

				} catch (InterruptedException e) {
					e.printStackTrace();
				} finally {
					
					if(locked){
						log("释放对象:lock");
						lock.unlock();
					}
				}
				log("线程结束");
			}
		};
		t2.setName("t2");
		t2.start();
	}

}

 线程交互

使用synchronized方式进行线程交互,用到的是同步对象的wait,notify和notifyAll方法

Lock也提供了类似的解决办法,首先通过lock对象得到一个Condition对象,然后分别调用这个Condition对象的:await, signal,signalAll 方法

注意: 不是Condition对象的wait,nofity,notifyAll方法,是await,signal,signalAll

package multiplethread;
 
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
public class TestThread {
 
    public static String now() {
        return new SimpleDateFormat("HH:mm:ss").format(new Date());
    }
 
    public static void log(String msg) {
        System.out.printf("%s %s %s %n", now() , Thread.currentThread().getName() , msg);
    }
 
    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        
        Thread t1 = new Thread() {
            public void run() {
                try {
                    log("线程启动");
                    log("试图占有对象:lock");
 
                    lock.lock();
 
                    log("占有对象:lock");
                    log("进行5秒的业务操作");
                    Thread.sleep(5000);
                    log("临时释放对象 lock, 并等待");
                    condition.await();
                    log("重新占有对象 lock,并进行5秒的业务操作");
                    Thread.sleep(5000);
 
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    log("释放对象:lock");
                    lock.unlock();
                }
                log("线程结束");
            }
        };
        t1.setName("t1");
        t1.start();
        try {
            //先让t1飞2秒
            Thread.sleep(2000);
        } catch (InterruptedException e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        }
        Thread t2 = new Thread() {
 
            public void run() {
                try {
                    log("线程启动");
                    log("试图占有对象:lock");
 
                    lock.lock();
 
                    log("占有对象:lock");
                    log("进行5秒的业务操作");
                    Thread.sleep(5000);
                    log("唤醒等待中的线程");
                    condition.signal();
 
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    log("释放对象:lock");
                    lock.unlock();
                }
                log("线程结束");
            }
        };
        t2.setName("t2");
        t2.start();
    }
 
}

 总结Lock和synchronized的区别

1. Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现,Lock是代码层面的实现。

2. Lock可以选择性的获取锁,如果一段时间获取不到,可以放弃。synchronized不行,会一根筋一直获取下去。 借助Lock的这个特性,就能够规避死锁,synchronized必须通过谨慎和良好的设计,才能减少死锁的发生。

3. synchronized在发生异常和同步块结束的时候,会自动释放锁。而Lock必须手动释放, 所以如果忘记了释放锁,一样会造成死锁。

9、原子访问

原子性操作概念

所谓的原子性操作即不可中断的操作,比如赋值操作

int i = 5;


原子性操作本身是线程安全的
但是 i++ 这个行为,事实上是有3个原子性操作组成的。
步骤 1. 取 i 的值
步骤 2. i + 1
步骤 3. 把新的值赋予i
这三个步骤,每一步都是一个原子操作,但是合在一起,就不是原子操作。就不是线程安全的。
换句话说,一个线程在步骤1 取i 的值结束后,还没有来得及进行步骤2,另一个线程也可以取 i的值了。
这也是分析同步问题产生的原因 中的原理。
i++ ,i--, i = i+1 这些都是非原子性操作。
只有int i = 1,这个赋值操作是原子性的。

Atomiclnteger

JDK6 以后,新增加了一个包java.util.concurrent.atomic,里面有各种原子类,比如AtomicInteger。
而AtomicInteger提供了各种自增,自减等方法,这些方法都是原子性的。 换句话说,自增方法 incrementAndGet 是线程安全的,同一个时间,只有一个线程可以调用这个方法。

package multiplethread;
  
import java.util.concurrent.atomic.AtomicInteger;
  
public class TestThread {
  
    public static void main(String[] args) throws InterruptedException {
    	AtomicInteger atomicI =new AtomicInteger();
    	int i = atomicI.decrementAndGet();
    	int j = atomicI.incrementAndGet();
    	int k = atomicI.addAndGet(3);
    	
    }
  
}

同步测试

分别使用基本变量的非原子性的++运算符和 原子性的AtomicInteger对象的 incrementAndGet 来进行多线程测试。
测试结果如图所示

package multiplethread;

import java.util.concurrent.atomic.AtomicInteger;

public class TestThread {
   
	private static int value = 0;
	private static AtomicInteger atomicValue =new AtomicInteger();
    public static void main(String[] args) {
    	int number = 100000;
    	Thread[] ts1 = new Thread[number];
    	for (int i = 0; i < number; i++) {
    		Thread t =new Thread(){
    			public void run(){
    				value++;
    			}
    		};
    		t.start();
    		ts1[i] = t;
		}
    	
    	//等待这些线程全部结束
    	for (Thread t : ts1) {
			try {
				t.join();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
    	
        System.out.printf("%d个线程进行value++后,value的值变成:%d%n", number,value);
      	Thread[] ts2 = new Thread[number];
    	for (int i = 0; i < number; i++) {
    		Thread t =new Thread(){
    			public void run(){
    				atomicValue.incrementAndGet();
    			}
    		};
    		t.start();
    		ts2[i] = t;
		}
    	
    	//等待这些线程全部结束
    	for (Thread t : ts2) {
			try {
				t.join();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
        System.out.printf("%d个线程进行atomicValue.incrementAndGet();后,atomicValue的值变成:%d%n", number,atomicValue.intValue());
    }
       
}

 1.7 JDBC

1、MySQL

JDBC (Java DataBase Connection) 是通过JAVA访问数据库
所以需要对数据库有基本的理解和应用

Mysql是常见的数据库,在中小型网站经常被使用。
如果以前没有接触过Mysql,请参考 MySQL入门 章节的学习

 其中包含了mysql 服务器安装客户端安装创建数据库创建表,以及SQL语句


2、Hello JDBC

为项目导入mysql-jdbc的jar包

初始化驱动

建立与数据库的连接

创建Statement

执行SQL语句

关闭连接

使用try-with-resource的方式自动关闭连接

3、增、删、改

CRUD是最常见的数据库操作,即增删改查
C 增加(Create)
R 读取查询(Retrieve)
U 更新(Update)
D 删除(Delete)

在JDBC中增加,删除,修改的操作都很类似,只是传递不同的SQL语句就行了。

查询因为要返回数据,所以和上面的不一样,将在查询章节讲解。

增加

这段代码就是Hello JDBC中的代码,用于向数据库中插入数据

package jdbc;
  
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
  
public class TestJDBC {
    public static void main(String[] args) {
  
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
  
        try (
            Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8",
                "root", "admin");
            Statement s = c.createStatement();              
        )
        {
            String sql = "insert into hero values(null," + "'提莫'" + "," + 313.0f + "," + 50 + ")";
            s.execute(sql);
              
        } catch (SQLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

删除

删除和增加很类似,只不过是执行的SQL语句不一样罢了

package jdbc;
  
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
  
public class TestJDBC {
    public static void main(String[] args) {
  
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
  
        try (
            Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8",
                "root", "admin");
            Statement s = c.createStatement();              
        )
        {
            String sql = "delete from hero where id = 5";
            s.execute(sql);
              
        } catch (SQLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

修改

修改也一样,执行另一条SQL语句就可以了

package jdbc;
  
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
  
public class TestJDBC {
    public static void main(String[] args) {
  
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
  
        try (
            Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8",
                "root", "admin");
            Statement s = c.createStatement();              
        )
        {
            String sql = "update hero set name = 'name 5' where id = 3";
            s.execute(sql);
              
        } catch (SQLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

4、查询

执行查询SQL语句

查询语句

executeQuery 执行SQL查询语句

注意: 在取第二列的数据的时候,用的是rs.get(2) ,而不是get(1). 这个是整个Java自带的api里唯二的地方,使用基1的,即2就代表第二个。

另一个地方是在PreparedStatement这里

package jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class TestJDBC {
	public static void main(String[] args) {
		try {
			Class.forName("com.mysql.jdbc.Driver");
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}

		try (Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8",
				"root", "admin"); Statement s = c.createStatement();) {

			String sql = "select * from hero";

			// 执行查询语句,并把结果集返回给ResultSet
			ResultSet rs = s.executeQuery(sql);
			while (rs.next()) {
				int id = rs.getInt("id");// 可以使用字段名
				String name = rs.getString(2);// 也可以使用字段的顺序
				float hp = rs.getFloat("hp");
				int damage = rs.getInt(4);
				System.out.printf("%d\t%s\t%f\t%d%n", id, name, hp, damage);
			}
			// 不一定要在这里关闭ReultSet,因为Statement关闭的时候,会自动关闭ResultSet
			// rs.close();

		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

 SQL语句判断账号密码是否正确

1. 创建一个用户表,有字段name,password
2. 插入一条数据

insert into user values(null,'dashen','thisispassword');


3. SQL语句判断账号密码是否正确

判断账号密码的正确方式是根据账号和密码到表中去找数据,如果有数据,就表明密码正确了,如果没数据,就表明密码错误。

不恰当的方式 是把uers表的数据全部查到内存中,挨个进行比较。 如果users表里有100万条数据呢? 内存都不够用的。

CREATE TABLE user (
  id int(11) AUTO_INCREMENT,
  name varchar(30) ,
  password varchar(30),
  PRIMARY KEY (id)
) ;
insert into user values(null,'dashen','thisispassword');

获取总数

执行的sql语句为

select count(*) from hero


然后通过ResultSet获取出来

package jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class TestJDBC {
	public static void main(String[] args) {
		try {
			Class.forName("com.mysql.jdbc.Driver");
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}

		try (Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8",
				"root", "admin"); Statement s = c.createStatement();) {

			String sql = "select count(*) from hero";

			ResultSet rs = s.executeQuery(sql);
			int total = 0;
			while (rs.next()) {
				total = rs.getInt(1);
			}

			System.out.println("表Hero中总共有:" + total+" 条数据");

		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}
}

 5、预编译Statement

使用PreparedStatement

和 Statement一样,PreparedStatement也是用来执行sql语句的
与创建Statement不同的是,需要根据sql语句创建PreparedStatement
除此之外,还能够通过设置参数,指定相应的值,而不是Statement那样使用字符串拼接

注: 这是JAVA里唯二的基1的地方,另一个是查询语句中的ResultSet也是基1的。

package jdbc;
   
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
   
public class TestJDBC {
    public static void main(String[] args) {
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
 
        String sql = "insert into hero values(null,?,?,?)";
        try (Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8","root", "admin");
        	// 根据sql语句创建PreparedStatement
            PreparedStatement ps = c.prepareStatement(sql);
        ) {
        	
            // 设置参数
            ps.setString(1, "提莫");
            ps.setFloat(2, 313.0f);
            ps.setInt(3, 50);
            // 执行
            ps.execute();
 
        } catch (SQLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
   
    }
}

PreparedStatement的优点1-参数设置

Statement 需要进行字符串拼接,可读性和维护性比较差

String sql = "insert into hero values(null,"+"'提莫'"+","+313.0f+","+50+")";


PreparedStatement 使用参数设置,可读性好,不易犯错

String sql = "insert into hero values(null,?,?,?)";

package jdbc;
 
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
 
public class TestJDBC {
    public static void main(String[] args) {
 
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
 
        String sql = "insert into hero values(null,?,?,?)";
        try (Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8","root", "admin"); 
            Statement s = c.createStatement(); 
            PreparedStatement ps = c.prepareStatement(sql);
        ) {
            // Statement需要进行字符串拼接,可读性和维修性比较差
            String sql0 = "insert into hero values(null," + "'提莫'" + "," + 313.0f + "," + 50 + ")";
            s.execute(sql0);
 
            // PreparedStatement 使用参数设置,可读性好,不易犯错
            // "insert into hero values(null,?,?,?)";
            ps.setString(1, "提莫");
            ps.setFloat(2, 313.0f);
            ps.setInt(3, 50);
            ps.execute();
        } catch (SQLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
 
    }
}

PreparedStatement的优点2-性能表现

PreparedStatement有预编译机制,性能比Statement更快

package jdbc;
 
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
 
public class TestJDBC {
    public static void main(String[] args) {
 
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
 
        String sql = "insert into hero values(null,?,?,?)";
        try (Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8","root", "admin"); 
                Statement s = c.createStatement(); 
                PreparedStatement ps = c.prepareStatement(sql);
            ) {
            // Statement执行10次,需要10次把SQL语句传输到数据库端
            // 数据库要对每一次来的SQL语句进行编译处理
            for (int i = 0; i < 10; i++) {
                String sql0 = "insert into hero values(null," + "'提莫'" + ","
                        + 313.0f + "," + 50 + ")";
                s.execute(sql0);
            }
            s.close();
 
            // PreparedStatement 执行10次,只需要1次把SQL语句传输到数据库端
            // 数据库对带?的SQL进行预编译
 
            // 每次执行,只需要传输参数到数据库端
            // 1. 网络传输量比Statement更小
            // 2. 数据库不需要再进行编译,响应更快
            for (int i = 0; i < 10; i++) {
                ps.setString(1, "提莫");
                ps.setFloat(2, 313.0f);
                ps.setInt(3, 50);
                ps.execute();
            }

		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
 
    }
}

PreparedStatement的优点3-防止SQL注入式攻击

假设name是用户提交来的数据
 
String name = "'盖伦' OR 1=1";
 

使用Statement就需要进行字符串拼接
拼接出来的语句是:
 
select * from hero where name = '盖伦' OR 1=1
 

因为有OR 1=1,这是恒成立的
那么就会把所有的英雄都查出来,而不只是盖伦
如果Hero表里的数据是海量的,比如几百万条,把这个表里的数据全部查出来
会让数据库负载变高,CPU100%,内存消耗光,响应变得极其缓慢

而PreparedStatement使用的是参数设置,就不会有这个问题

package jdbc;
 
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
 
public class TestJDBC {
    public static void main(String[] args) {
 
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
 
        String sql = "select * from hero where name = ?";
        try (Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8","root", "admin");
        		Statement s = c.createStatement();
            PreparedStatement ps = c.prepareStatement(sql);
        ) {
            // 假设name是用户提交来的数据
            String name = "'盖伦' OR 1=1";
            String sql0 = "select * from hero where name = " + name;
            // 拼接出来的SQL语句就是
            // select * from hero where name = '盖伦' OR 1=1
            // 因为有OR 1=1,所以恒成立
            // 那么就会把所有的英雄都查出来,而不只是盖伦
            // 如果Hero表里的数据是海量的,比如几百万条,把这个表里的数据全部查出来
            // 会让数据库负载变高,CPU100%,内存消耗光,响应变得极其缓慢
            System.out.println(sql0);
 
            ResultSet rs0 = s.executeQuery(sql0);
            while (rs0.next()) {
                String heroName = rs0.getString("name");
                System.out.println(heroName);
            }
 
            s.execute(sql0);
 
            // 使用预编译Statement就可以杜绝SQL注入
 
            ps.setString(1, name);
 
            ResultSet rs = ps.executeQuery();
            // 查不出数据出来
            while (rs.next()) {
                String heroName = rs.getString("name");
                System.out.println(heroName);
            }

		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
 
    }
}

6、execute executeUpdate

execute与executeUpdate的区别

相同点

execute与executeUpdate的相同点:都可以执行增加,删除,修改

package jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class TestJDBC {
	public static void main(String[] args) {
		try {
			Class.forName("com.mysql.jdbc.Driver");
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}

		try (Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8","root", "admin");
			Statement s = c.createStatement();) {

			String sqlInsert = "insert into Hero values (null,'盖伦',616,100)";
			String sqlDelete = "delete from Hero where id = 100";
			String sqlUpdate = "update Hero set hp = 300 where id = 100";

			// 相同点:都可以执行增加,删除,修改

			s.execute(sqlInsert);
			s.execute(sqlDelete);
			s.execute(sqlUpdate);
			s.executeUpdate(sqlInsert);
			s.executeUpdate(sqlDelete);
			s.executeUpdate(sqlUpdate);

		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}
}

不同点

不同1:
execute可以执行查询语句
然后通过getResultSet,把结果集取出来
executeUpdate不能执行查询语句
不同2:
execute返回boolean类型,true表示执行的是查询语句,false表示执行的是insert,delete,update等等
executeUpdate返回的是int,表示有多少条数据受到了影响

package jdbc;
 
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
 
public class TestJDBC {
    public static void main(String[] args) {
 
		try {
			Class.forName("com.mysql.jdbc.Driver");
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}

		try (Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8","root", "admin");
			Statement s = c.createStatement();) {
 
            // 不同1:execute可以执行查询语句
            // 然后通过getResultSet,把结果集取出来
            String sqlSelect = "select * from hero";
 
            s.execute(sqlSelect);
            ResultSet rs = s.getResultSet();
            while (rs.next()) {
                System.out.println(rs.getInt("id"));
            }
 
            // executeUpdate不能执行查询语句
            // s.executeUpdate(sqlSelect);
 
            // 不同2:
            // execute返回boolean类型,true表示执行的是查询语句,false表示执行的是insert,delete,update等等
            boolean isSelect = s.execute(sqlSelect);
            System.out.println(isSelect);
 
            // executeUpdate返回的是int,表示有多少条数据受到了影响
            String sqlUpdate = "update Hero set hp = 300 where id < 100";
            int number = s.executeUpdate(sqlUpdate);
            System.out.println(number);
 
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
 
    }
}

7、特殊操作

获取自增长

在Statement通过execute或者executeUpdate执行完插入语句后,MySQL会为新插入的数据分配一个自增长id,(前提是这个表的id设置为了自增长,在Mysql创建表的时候,AUTO_INCREMENT就表示自增长)

CREATE TABLE hero (

id int(11) AUTO_INCREMENT,

...

}
但是无论是execute还是executeUpdate都不会返回这个自增长id是多少。需要通过Statement的getGeneratedKeys获取该id
注: 第20行的代码,后面加了个Statement.RETURN_GENERATED_KEYS参数,以确保会返回自增长ID。 通常情况下不需要加这个,有的时候需要加,所以先加上,保险一些

PreparedStatement ps = c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);

package jdbc;
  
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
  
public class TestJDBC {
  
    public static void main(String[] args) {
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
         String sql = "insert into hero values(null,?,?,?)";
        try (Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8","root", "admin");
        		PreparedStatement ps = c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);           
                ) {
 
            ps.setString(1, "盖伦");
            ps.setFloat(2, 616);
            ps.setInt(3, 100);
  
            // 执行插入语句
            ps.execute();
  
            // 在执行完插入语句后,MySQL会为新插入的数据分配一个自增长id
            // JDBC通过getGeneratedKeys获取该id
            ResultSet rs = ps.getGeneratedKeys();
            if (rs.next()) {
                int id = rs.getInt(1);
                System.out.println(id);
            }
  
        } catch (SQLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
  
    }
}

获取表的元数据

元数据概念:
和数据库服务器相关的数据,比如数据库版本,有哪些表,表有哪些字段,字段类型是什么等等。

package jdbc;
 
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
 
public class TestJDBC {
 
    public static void main(String[] args) throws Exception {
		try {
			Class.forName("com.mysql.jdbc.Driver");
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}

		try (Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8","root", "admin");) {
 
            // 查看数据库层面的元数据
            // 即数据库服务器版本,驱动版本,都有哪些数据库等等
 
            DatabaseMetaData dbmd = c.getMetaData();
 
            // 获取数据库服务器产品名称
            System.out.println("数据库产品名称:\t"+dbmd.getDatabaseProductName());
            // 获取数据库服务器产品版本号
            System.out.println("数据库产品版本:\t"+dbmd.getDatabaseProductVersion());
            // 获取数据库服务器用作类别和表名之间的分隔符 如test.user
            System.out.println("数据库和表分隔符:\t"+dbmd.getCatalogSeparator());
            // 获取驱动版本
            System.out.println("驱动版本:\t"+dbmd.getDriverVersion());
 
            System.out.println("可用的数据库列表:");
            // 获取数据库名称
            ResultSet rs = dbmd.getCatalogs();
 
            while (rs.next()) {
                System.out.println("数据库名称:\t"+rs.getString(1));
            }
 
        } catch (SQLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
 
    }
}

8、事务

不使用事务的情况

没有事务的前提下
假设业务操作是:加血,减血各做一次
结束后,英雄的血量不变
而减血的SQL
不小心写错写成了 updata(而非update)
那么最后结果是血量增加了,而非期望的不变

package jdbc;
 
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
 
public class TestJDBC {
    public static void main(String[] args) {
		try {
			Class.forName("com.mysql.jdbc.Driver");
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}

		try (Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8","root", "admin");
			Statement s = c.createStatement();) {
 
            //没有事务的前提下
            //假设业务操作时,加血,减血各做一次
            //结束后,英雄的血量不变
             
            //加血的SQL
            String sql1 = "update hero set hp = hp +1 where id = 22";
            s.execute(sql1);
             
            //减血的SQL
            //不小心写错写成了 updata(而非update)
             
            String sql2 = "updata hero set hp = hp -1 where id = 22";
            s.execute(sql2);
 
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
 
    }
}

使用事务

在事务中的多个操作,要么都成功,要么都失败
通过 c.setAutoCommit(false);关闭自动提交
使用 c.commit();进行手动提交
在22行-35行之间的数据库操作,就处于同一个事务当中,要么都成功,要么都失败
所以,虽然第一条SQL语句是可以执行的,但是第二条SQL语句有错误,其结果就是两条SQL语句都没有被提交。 除非两条SQL语句都是正确的。

package jdbc;
 
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
 
public class TestJDBC {
    public static void main(String[] args) {
		try {
			Class.forName("com.mysql.jdbc.Driver");
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}

		try (Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8","root", "admin");
			Statement s = c.createStatement();) {
 
            // 有事务的前提下
            // 在事务中的多个操作,要么都成功,要么都失败
 
            c.setAutoCommit(false);
 
            // 加血的SQL
            String sql1 = "update hero set hp = hp +1 where id = 22";
            s.execute(sql1);
 
            // 减血的SQL
            // 不小心写错写成了 updata(而非update)
 
            String sql2 = "updata hero set hp = hp -1 where id = 22";
            s.execute(sql2);
 
            // 手动提交
            c.commit();
 
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
 
    }
}

MYSQL表的类型必须是INNODB才支持事务

在Mysql中,只有当表的类型是INNODB的时候,才支持事务,所以需要把表的类型设置为INNODB,否则无法观察到事务.

修改表的类型为INNODB的SQL:

alter table hero ENGINE = innodb;
查看表的类型的SQL

show table status from how2java;


不过有个前提,就是当前的MYSQL服务器本身要支持INNODB,如果不支持,请看 开启MYSQL INNODB的办法

9、ORM

ORM=Object Relationship Database Mapping

对象和关系数据库的映射

简单说,一个对象,对应数据库里的一条记录

根据id返回一个Hero对象

提供方法get(int id)
返回一个Hero对象

package charactor;

public class Hero {
	//增加id属性
	public int id;
	public String name;
	public float hp;
	public int damage;

}

10、DAO

DAO=DataAccess Object

数据访问对象

实际上就是运用了练习-ORM中的思路,把数据库相关的操作都封装在这个类里面,其他地方看不到JDBC的代码

DAO接口

package jdbc;
 
import java.util.List;

import charactor.Hero;
 
public interface DAO{
    //增加
    public void add(Hero hero);
    //修改
    public void update(Hero hero);
    //删除
    public void delete(int id);
    //获取
    public Hero get(int id);
    //查询
    public List<Hero> list();
    //分页查询
    public List<Hero> list(int start, int count);
}

HeroDAO

设计类HeroDAO,实现接口DAO

这个HeroDAO和答案-ORM很接近,做了几个改进:
1. 把驱动的初始化放在了构造方法HeroDAO里:

public HeroDAO() {

try {

Class.forName("com.mysql.jdbc.Driver");

} catch (ClassNotFoundException e) {

e.printStackTrace();

}

}


因为驱动初始化只需要执行一次,所以放在这里更合适,其他方法里也不需要写了,代码更简洁

2. 提供了一个getConnection方法返回连接
所有的数据库操作都需要事先拿到一个数据库连接Connection,以前的做法每个方法里都会写一个,如果要改动密码,那么每个地方都需要修改。 通过这种方式,只需要修改这一个地方就可以了。 代码变得更容易维护,而且也更加简洁。

package jdbc;
 
import java.sql.Connection;

import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
 
import charactor.Hero;
 
public class HeroDAO implements DAO{
 
    public HeroDAO() {
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
 
    public Connection getConnection() throws SQLException {
        return DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8", "root",
                "admin");
    }
 
    public int getTotal() {
        int total = 0;
        try (Connection c = getConnection(); Statement s = c.createStatement();) {
 
            String sql = "select count(*) from hero";
 
            ResultSet rs = s.executeQuery(sql);
            while (rs.next()) {
                total = rs.getInt(1);
            }
 
            System.out.println("total:" + total);
 
        } catch (SQLException e) {
 
            e.printStackTrace();
        }
        return total;
    }
 
    public void add(Hero hero) {
 
        String sql = "insert into hero values(null,?,?,?)";
        try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) {
 
            ps.setString(1, hero.name);
            ps.setFloat(2, hero.hp);
            ps.setInt(3, hero.damage);
 
            ps.execute();
 
            ResultSet rs = ps.getGeneratedKeys();
            if (rs.next()) {
                int id = rs.getInt(1);
                hero.id = id;
            }
        } catch (SQLException e) {
 
            e.printStackTrace();
        }
    }
 
    public void update(Hero hero) {
 
        String sql = "update hero set name= ?, hp = ? , damage = ? where id = ?";
        try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) {
 
            ps.setString(1, hero.name);
            ps.setFloat(2, hero.hp);
            ps.setInt(3, hero.damage);
            ps.setInt(4, hero.id);
 
            ps.execute();
 
        } catch (SQLException e) {
 
            e.printStackTrace();
        }
 
    }
 
    public void delete(int id) {
 
        try (Connection c = getConnection(); Statement s = c.createStatement();) {
 
            String sql = "delete from hero where id = " + id;
 
            s.execute(sql);
 
        } catch (SQLException e) {
 
            e.printStackTrace();
        }
    }
 
    public Hero get(int id) {
        Hero hero = null;
 
        try (Connection c = getConnection(); Statement s = c.createStatement();) {
 
            String sql = "select * from hero where id = " + id;
 
            ResultSet rs = s.executeQuery(sql);
 
            if (rs.next()) {
                hero = new Hero();
                String name = rs.getString(2);
                float hp = rs.getFloat("hp");
                int damage = rs.getInt(4);
                hero.name = name;
                hero.hp = hp;
                hero.damage = damage;
                hero.id = id;
            }
 
        } catch (SQLException e) {
 
            e.printStackTrace();
        }
        return hero;
    }
 
    public List<Hero> list() {
        return list(0, Short.MAX_VALUE);
    }
 
    public List<Hero> list(int start, int count) {
        List<Hero> heros = new ArrayList<Hero>();
 
        String sql = "select * from hero order by id desc limit ?,? ";
 
        try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) {
 
            ps.setInt(1, start);
            ps.setInt(2, count);
 
            ResultSet rs = ps.executeQuery();
 
            while (rs.next()) {
                Hero hero = new Hero();
                int id = rs.getInt(1);
                String name = rs.getString(2);
                float hp = rs.getFloat("hp");
                int damage = rs.getInt(4);
                hero.id = id;
                hero.name = name;
                hero.hp = hp;
                hero.damage = damage;
                heros.add(hero);
            }
        } catch (SQLException e) {
 
            e.printStackTrace();
        }
        return heros;
    }
 
}

11、数据库连接池

线程池类似的,数据库也有一个数据库连接池。 不过他们的实现思路是不一样的。
本章节讲解了自定义数据库连接池类:ConnectionPool,虽然不是很完善和健壮,但是足以帮助大家理解ConnectionPool的基本原理。

数据库连接池原理-传统方式

当有多个线程,每个线程都需要连接数据库执行SQL语句的话,那么每个线程都会创建一个连接,并且在使用完毕后,关闭连接。

创建连接和关闭连接的过程也是比较消耗时间的,当多线程并发的时候,系统就会变得很卡顿。

同时,一个数据库同时支持的连接总数也是有限的,如果多线程并发量很大,那么数据库连接的总数就会被消耗光,后续线程发起的数据库连接就会失败。

数据库连接池原理-使用池

与传统方式不同,连接池在使用之前,就会创建好一定数量的连接。
如果有任何线程需要使用连接,那么就从连接池里面借用,而不是自己重新创建.
使用完毕后,又把这个连接归还给连接池供下一次或者其他线程使用。
倘若发生多线程并发情况,连接池里的连接被借用光了,那么其他线程就会临时等待,直到有连接被归还回来,再继续使用。
整个过程,这些连接都不会被关闭,而是不断的被循环使用,从而节约了启动和关闭连接的时间。

ConnectionPoot构造方法和初始化

1. ConnectionPool() 构造方法约定了这个连接池一共有多少连接

2. 在init() 初始化方法中,创建了size条连接。 注意,这里不能使用try-with-resource这种自动关闭连接的方式,因为连接恰恰需要保持不关闭状态,供后续循环使用

3. getConnection, 判断是否为空,如果是空的就wait等待,否则就借用一条连接出去

4. returnConnection, 在使用完毕后,归还这个连接到连接池,并且在归还完毕后,调用notifyAll,通知那些等待的线程,有新的连接可以借用了。

注:连接池设计用到了多线程的wait和notifyAll,这些内容可以在多线程交互章节查阅学习。

package jdbc;
 
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
 
public class ConnectionPool {
 
    List<Connection> cs = new ArrayList<Connection>();
 
    int size;
 
    public ConnectionPool(int size) {
        this.size = size;
        init();
    }
 
    public void init() {
         
        //这里恰恰不能使用try-with-resource的方式,因为这些连接都需要是"活"的,不要被自动关闭了
        try {
            Class.forName("com.mysql.jdbc.Driver");
            for (int i = 0; i < size; i++) {
                Connection c = DriverManager
                        .getConnection("jdbc:mysql://127.0.0.1:3306/how2java?characterEncoding=UTF-8", "root", "admin");
 
                cs.add(c);
 
            }
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (SQLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
 
    public synchronized Connection getConnection() {
        while (cs.isEmpty()) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        Connection c = cs.remove(0);
        return c;
    }
 
    public synchronized void returnConnection(Connection c) {
        cs.add(c);
        this.notifyAll();
    }
 
}

测试类

首先初始化一个有3条连接的数据库连接池
然后创建100个线程,每个线程都会从连接池中借用连接,并且在借用之后,归还连接。 拿到连接之后,执行一个耗时1秒的SQL语句。

运行程序,就可以观察到如图所示的效果

package jdbc;
 
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;

import jdbc.ConnectionPool;
  
public class TestConnectionPool {
  
    public static void main(String[] args) {
        ConnectionPool cp = new ConnectionPool(3);
        for (int i = 0; i < 100; i++) {
            new WorkingThread("working thread" + i, cp).start();
        }
  
    }
}
  
class WorkingThread extends Thread {
    private ConnectionPool cp;
  
    public WorkingThread(String name, ConnectionPool cp) {
        super(name);
        this.cp = cp;
    }
  
    public void run() {
        Connection c = cp.getConnection();
        System.out.println(this.getName()+ ":\t 获取了一根连接,并开始工作"  );
        try (Statement st = c.createStatement()){
            
            //模拟时耗1秒的数据库SQL语句
            Thread.sleep(1000);
            st.execute("select * from hero");
  
        } catch (SQLException | InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        cp.returnConnection(c);
    }
}

1.8 图形界面

GUI-Graphic User Interface 图形用户界面

1、Hello Swing

简单的例子

JFrame是GUI中的容器
JButton是最常见的组件- 按钮
注意:f.setVisible(true); 会对所有的组件进行渲染,所以一定要放在最后面

package gui;

import javax.swing.JButton;
import javax.swing.JFrame;

public class TestGUI {
	public static void main(String[] args) {
		// 主窗体
		JFrame f = new JFrame("LoL");

		// 主窗体设置大小
		f.setSize(400, 300);

		// 主窗体设置位置
		f.setLocation(200, 200);

		// 主窗体中的组件设置为绝对定位
		f.setLayout(null);

		// 按钮组件
		JButton b = new JButton("一键秒对方基地挂");

		// 同时设置组件的大小和位置
		b.setBounds(50, 50, 280, 30);

		// 把按钮加入到主窗体中
		f.add(b);

		// 关闭窗体的时候,退出程序
		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		// 让窗体变得可见
		f.setVisible(true);

	}
}

2、事件监听

按钮监听

创建一个匿名类实现ActionListener接口,当按钮被点击时,actionPerformed方法就会被调用

package gui;
 
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
 
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
 
public class TestGUI {
    public static void main(String[] args) {
 
        JFrame f = new JFrame("LoL");
        f.setSize(400, 300);
        f.setLocation(580, 200);
        f.setLayout(null);
 
        final JLabel l = new JLabel();
 
        ImageIcon i = new ImageIcon("e:/project/j2se/shana.png");
        l.setIcon(i);
        l.setBounds(50, 50, i.getIconWidth(), i.getIconHeight());
 
        JButton b = new JButton("隐藏图片");
        b.setBounds(150, 200, 100, 30);
 
        // 给按钮 增加 监听
        b.addActionListener(new ActionListener() {
 
            // 当按钮被点击时,就会触发 ActionEvent事件
            // actionPerformed 方法就会被执行
            public void actionPerformed(ActionEvent e) {
                l.setVisible(false);
            }
        });
 
        f.add(l);
        f.add(b);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
        f.setVisible(true);
    }
}

键盘监听

键盘监听器: KeyListener
keyPressed 代表 键被按下
keyReleased 代表 键被弹起
keyTyped 代表 一个按下弹起的组合动作
KeyEvent.getKeyCode() 可以获取当前点下了哪个键

package gui;
 
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
 
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
 
public class TestGUI {
    public static void main(String[] args) {
 
        JFrame f = new JFrame("LoL");
        f.setSize(400, 300);
        f.setLocation(580, 200);
        f.setLayout(null);
 
        final JLabel l = new JLabel();
 
        ImageIcon i = new ImageIcon("e:/project/j2se/shana.png");
        l.setIcon(i);
        l.setBounds(50, 50, i.getIconWidth(), i.getIconHeight());
 
        // 增加键盘监听
        f.addKeyListener(new KeyListener() {
 
        	// 键被弹起
            public void keyReleased(KeyEvent e) {
 
                // 39代表按下了 “右键”
                if (e.getKeyCode() == 39) {
                    // 图片向右移动 (y坐标不变,x坐标增加)
                    l.setLocation(l.getX() + 10, l.getY());
                }
            }
 
            //键被按下
            public void keyPressed(KeyEvent e) {
                // TODO Auto-generated method stub
 
            }
 
            // 一个按下弹起的组合动作
            public void keyTyped(KeyEvent e) {
 
            }
        });
 
        f.add(l);
 
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
        f.setVisible(true);
    }
}

鼠标监听

MouseListener 鼠标监听器
mouseReleased 鼠标释放
mousePressed 鼠标按下
mouseExited 鼠标退出
mouseEntered 鼠标进入
mouseClicked 鼠标点击
在本例中,使用mouseEntered,当鼠标进入图片的时候,图片就移动位置

package gui;
 
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.Random;
 
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
 
public class TestGUI {
    public static void main(String[] args) {
 
        final JFrame f = new JFrame("LoL");
        f.setSize(800, 600);
        f.setLocationRelativeTo(null);
        f.setLayout(null);
 
        final JLabel l = new JLabel();
        ImageIcon i = new ImageIcon("e:/project/j2se/shana_heiheihei.png");
        l.setIcon(i);
        l.setBounds(375, 275, i.getIconWidth(), i.getIconHeight());
 
        f.add(l);
 
        l.addMouseListener(new MouseListener() {
 
            // 释放鼠标
            public void mouseReleased(MouseEvent e) {
                // TODO Auto-generated method stub
 
            }
 
            // 按下鼠标
            public void mousePressed(MouseEvent e) {
                // TODO Auto-generated method stub
 
            }
 
            // 鼠标退出
            public void mouseExited(MouseEvent e) {
                // TODO Auto-generated method stub
 
            }
 
            // 鼠标进入
            public void mouseEntered(MouseEvent e) {
 
                Random r = new Random();
 
                int x = r.nextInt(f.getWidth() - l.getWidth());
                int y = r.nextInt(f.getHeight() - l.getHeight());
 
                l.setLocation(x, y);
 
            }
 
            // 按下释放组合动作为点击鼠标
            public void mouseClicked(MouseEvent e) {
                // TODO Auto-generated method stub
 
            }
        });
 
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
        f.setVisible(true);
    }
}

适配器

MouseAdapter 鼠标监听适配器
一般说来在写监听器的时候,会实现MouseListener。
但是MouseListener里面有很多方法实际上都没有用到,比如mouseReleased ,mousePressed,mouseExited等等。
这个时候就可以使用 鼠标监听适配器,MouseAdapter 只需要重写必要的方法即可。

package gui;
 
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Random;
 
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
 
public class TestGUI {
    public static void main(String[] args) {
 
        final JFrame f = new JFrame("LoL");
        f.setSize(800, 600);
        f.setLocationRelativeTo(null);
        f.setLayout(null);
 
        final JLabel l = new JLabel("");
 
        ImageIcon i = new ImageIcon("e:/project/j2se/shana_heiheihei.png");
        l.setIcon(i);
        l.setBounds(375, 275, i.getIconWidth(), i.getIconHeight());
 
        f.add(l);
 
        // MouseAdapter 适配器,只需要重写用到的方法,没有用到的就不用写了
        l.addMouseListener(new MouseAdapter() {
 
            // 只有mouseEntered用到了
            public void mouseEntered(MouseEvent e) {
 
                Random r = new Random();
 
                int x = r.nextInt(f.getWidth() - l.getWidth());
                int y = r.nextInt(f.getHeight() - l.getHeight());
 
                l.setLocation(x, y);
 
            }
        });
 
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
        f.setVisible(true);
    }
}

3、容器

java的图形界面中,容器是用来存放 按钮,输入框等组件的。

窗体型容器有两个,一个是JFrame,一个是JDialog

JFrame

JFrame是最常用的窗体型容器,默认情况下,在右上角有最大化最小化按钮

package gui;

import javax.swing.JButton;
import javax.swing.JFrame;

public class TestGUI {
	public static void main(String[] args) {
		
		//普通的窗体,带最大和最小化按钮
		JFrame f = new JFrame("LoL");
		f.setSize(400, 300);
		f.setLocation(200, 200);
		f.setLayout(null);
		JButton b = new JButton("一键秒对方基地挂");
		b.setBounds(50, 50, 280, 30);

		f.add(b);
		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);
	}
}

JDialog

JDialog也是窗体型容器,右上角没有最大和最小化按钮

package gui;

import javax.swing.JButton;
import javax.swing.JDialog;

public class TestGUI {
	public static void main(String[] args) {
		
		//普通的窗体,带最大和最小化按钮,而对话框却不带
		JDialog d = new JDialog();
		d.setTitle("LOL");
		d.setSize(400, 300);
		d.setLocation(200, 200);
		d.setLayout(null);
		JButton b = new JButton("一键秒对方基地挂");
		b.setBounds(50, 50, 280, 30);

		d.add(b);

		d.setVisible(true);
	}
}

模态JDialog

当一个对话框被设置为模态的时候,其背后的父窗体,是不能被激活的,除非该对话框被关闭

package gui;

import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFrame;

public class TestGUI {
	public static void main(String[] args) {
		JFrame f = new JFrame("外部窗体");
		f.setSize(800, 600);
		f.setLocation(100, 100);

		// 根据外部窗体实例化JDialog
		JDialog d = new JDialog(f);
		// 设置为模态
		d.setModal(true);

		d.setTitle("模态的对话框");
		d.setSize(400, 300);
		d.setLocation(200, 200);
		d.setLayout(null);
		JButton b = new JButton("一键秒对方基地挂");
		b.setBounds(50, 50, 280, 30);
		d.add(b);

		f.setVisible(true);
		d.setVisible(true);

	}
}

窗体大小不可变化

通过调用方法 setResizable(false); 做到窗体大小不可变化

package gui;

import javax.swing.JButton;
import javax.swing.JFrame;

public class TestGUI {
	public static void main(String[] args) {

		JFrame f = new JFrame("LoL");
		f.setSize(400, 300);
		f.setLocation(200, 200);
		f.setLayout(null);
		JButton b = new JButton("一键秒对方基地挂");
		b.setBounds(50, 50, 280, 30);

		f.add(b);
		// 窗体大小不可变化
		f.setResizable(false);
		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);
	}
}

4、布局器

布局器是用在容器上的。 用来决定容器上的组件摆放的位置和大小

绝对定位

绝对定位就是指不使用布局器,组件的位置和大小需要单独指定

package gui;

import javax.swing.JButton;
import javax.swing.JFrame;

public class TestGUI {
	public static void main(String[] args) {

		JFrame f = new JFrame("LoL");
		f.setSize(400, 300);
		f.setLocation(200, 200);
		// 设置布局器为null,即进行绝对定位,容器上的组件都需要指定位置和大小
		f.setLayout(null);
		JButton b1 = new JButton("英雄1");
		// 指定位置和大小
		b1.setBounds(50, 50, 80, 30);
		JButton b2 = new JButton("英雄2");
		b2.setBounds(150, 50, 80, 30);
		JButton b3 = new JButton("英雄3");
		b3.setBounds(250, 50, 80, 30);
		// 没有指定位置和大小,不会出现在容器上
		JButton b4 = new JButton("英雄3");

		f.add(b1);
		f.add(b2);
		f.add(b3);
		// b4没有指定位置和大小,不会出现在容器上
		f.add(b4);
		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);
	}
}

FlowLayout

设置布局器为FlowLayout,顺序布局器
容器上的组件水平摆放
加入到容器即可,无需单独指定大小和位置

package gui;

import java.awt.FlowLayout;

import javax.swing.JButton;
import javax.swing.JFrame;

public class TestGUI {
	public static void main(String[] args) {

		JFrame f = new JFrame("LoL");
		f.setSize(400, 300);
		f.setLocation(200, 200);
		// 设置布局器为FlowLayerout
		// 容器上的组件水平摆放
		f.setLayout(new FlowLayout());

		JButton b1 = new JButton("英雄1");
		JButton b2 = new JButton("英雄2");
		JButton b3 = new JButton("英雄3");

		// 加入到容器即可,无需单独指定大小和位置
		f.add(b1);
		f.add(b2);
		f.add(b3);

		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);
	}
}

BorderLayout

设置布局器为BorderLayout
容器上的组件按照上北 下南 左西 右东 中的顺序摆放

package gui;

import java.awt.BorderLayout;
import java.awt.FlowLayout;

import javax.swing.JButton;
import javax.swing.JFrame;

public class TestGUI {
	public static void main(String[] args) {

		JFrame f = new JFrame("LoL");
		f.setSize(400, 300);
		f.setLocation(200, 200);
		// 设置布局器为BorderLayerout
		// 容器上的组件按照上北下南左西右东中的顺序摆放
		f.setLayout(new BorderLayout());

		JButton b1 = new JButton("洪七");
		JButton b2 = new JButton("段智兴");
		JButton b3 = new JButton("欧阳锋");
		JButton b4 = new JButton("黄药师");
		JButton b5 = new JButton("周伯通");

		// 加入到容器的时候,需要指定位置
		f.add(b1, BorderLayout.NORTH);
		f.add(b2, BorderLayout.SOUTH);
		f.add(b3, BorderLayout.WEST);
		f.add(b4, BorderLayout.EAST);
		f.add(b5, BorderLayout.CENTER);

		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);
	}
}

GridLayout

GridLayout,即网格布局器

package gui;

import java.awt.GridLayout;

import javax.swing.JButton;
import javax.swing.JFrame;

public class TestGUI {
	public static void main(String[] args) {

		JFrame f = new JFrame("LoL");
		f.setSize(400, 300);
		f.setLocation(200, 200);
		// 设置布局器为GridLayerout,即网格布局器
		// 该GridLayerout的构造方法表示该网格是2行3列
		f.setLayout(new GridLayout(2, 3));

		JButton b1 = new JButton("洪七");
		JButton b2 = new JButton("段智兴");
		JButton b3 = new JButton("欧阳锋");
		JButton b4 = new JButton("黄药师");
		JButton b5 = new JButton("周伯通");

		f.add(b1);
		f.add(b2);
		f.add(b3);
		f.add(b4);
		f.add(b5);

		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);
	}
}

setPreferredSize

即便 使用 布局器 ,也可以 通过setPreferredSize,向布局器建议该组件显示的大小.
注 只对部分布局器起作用,比如FlowLayout可以起作用。 比如GridLayout就不起作用,因为网格布局器必须对齐

package gui;

import java.awt.Dimension;
import java.awt.FlowLayout;

import javax.swing.JButton;
import javax.swing.JFrame;

public class TestGUI {
	public static void main(String[] args) {

		JFrame f = new JFrame("LoL");
		f.setSize(400, 300);
		f.setLocation(200, 200);
		f.setLayout(new FlowLayout());

		JButton b1 = new JButton("英雄1");
		JButton b2 = new JButton("英雄2");
		JButton b3 = new JButton("英雄3");

		// 即便 使用 布局器 ,也可以 通过setPreferredSize,向布局器建议该组件显示的大小
		b3.setPreferredSize(new Dimension(180, 40));

		f.add(b1);
		f.add(b2);
		f.add(b3);

		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);
	}
}

CardLayout

因为CardLayout需要用到面板JComboBox这些内容暂时还没学的内容,所以放在后面讲: ​​​​​​CardLayout


 

5、组件

JAVA的图形界面下有两组控件,一组是awt,一组是swing。一般都是使用swing。

标签

Label用于显示文字

package gui;
 
import java.awt.Color;

import javax.swing.JFrame;
import javax.swing.JLabel;
 
public class TestGUI {
    public static void main(String[] args) {
         
        JFrame f = new JFrame("LoL");
        f.setSize(400, 300);
        f.setLocation(200, 200);
        f.setLayout(null);
        JLabel l = new JLabel("LOL文字");
        //文字颜色
        l.setForeground(Color.red);
        l.setBounds(50, 50, 280, 30);
 
        f.add(l);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
        f.setVisible(true);
    }
}

使用JLabel显示图片

java GUI 显示图片是通过在label上设置图标实现的
注: 这里的图片路径是e:/project/j2se/shana.png,所以要确保这里有图片,不然不会显示
注: 图片的后缀名和真实格式,必须保持一致,否则很有可能无法正常显示。 什么意思呢?就是文件本来是jpg的,但是仅仅是把后缀名修改成了png,那么会导致显示失败。

package gui;

import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;

public class TestGUI {
	public static void main(String[] args) {

		JFrame f = new JFrame("LoL");
		f.setSize(400, 300);
		f.setLocation(580, 200);
		f.setLayout(null);

		JLabel l = new JLabel();

		//根据图片创建ImageIcon对象
		ImageIcon i = new ImageIcon("e:/project/j2se/shana.png");
		//设置ImageIcon
		l.setIcon(i);
		//label的大小设置为ImageIcon,否则显示不完整
		l.setBounds(50, 50, i.getIconWidth(), i.getIconHeight());

		f.add(l);
		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);
	}
}

按钮

JButton 普通按钮

package gui;
 
import javax.swing.JButton;
import javax.swing.JFrame;
 
public class TestGUI {
    public static void main(String[] args) {
         
        JFrame f = new JFrame("LoL");
        f.setSize(400, 300);
        f.setLocation(200, 200);
        f.setLayout(null);
        JButton b = new JButton("一键秒对方基地挂");
        b.setBounds(50, 50, 280, 30);
 
        f.add(b);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
        f.setVisible(true);
    }
}

复选框

JCheckBox 复选框

使用isSelected来获取是否选中了

package gui;

import javax.swing.JCheckBox;
import javax.swing.JFrame;

public class TestGUI {
	public static void main(String[] args) {

		JFrame f = new JFrame("LoL");
		f.setSize(400, 300);
		f.setLocation(580, 200);
		f.setLayout(null);
		JCheckBox bCheckBox = new JCheckBox("物理英雄");
		//设置 为 默认被选中 
		bCheckBox.setSelected(true);
		bCheckBox.setBounds(50, 50, 130, 30);
		JCheckBox bCheckBox2 = new JCheckBox("魔法 英雄");
		bCheckBox2.setBounds(50, 100, 130, 30);
		//判断 是否 被 选中 
		System.out.println(bCheckBox2.isSelected());

		f.add(bCheckBox);
		f.add(bCheckBox2);
		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);
	}
}

单选框

JRadioButton 单选框
使用isSelected来获取是否选中了

在这个例子里,两个单选框可以被同时选中,为了实现只能选中一个,还需要用到

ButtonGroup

package gui;

import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JRadioButton;

public class TestGUI {
	public static void main(String[] args) {

		JFrame f = new JFrame("LoL");
		f.setSize(400, 300);
		f.setLocation(580, 200);
		f.setLayout(null);
		JRadioButton b1 = new JRadioButton("物理英雄");
		// 设置 为 默认被选中
		b1.setSelected(true);
		b1.setBounds(50, 50, 130, 30);
		JRadioButton b2 = new JRadioButton("魔法 英雄");
		b2.setBounds(50, 100, 130, 30);

		System.out.println(b2.isSelected());

		f.add(b1);
		f.add(b2);
		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);
	}
}

按钮组

ButtonGroup 对按钮进行分组,把不同的按钮,放在同一个分组里 ,同一时间,只有一个 按钮 会被选中

package gui;

import javax.swing.ButtonGroup;
import javax.swing.JFrame;
import javax.swing.JRadioButton;

public class TestGUI {
	public static void main(String[] args) {

		JFrame f = new JFrame("LoL");
		f.setSize(400, 300);
		f.setLocation(580, 240);
		f.setLayout(null);
		JRadioButton b1 = new JRadioButton("物理英雄");
		b1.setSelected(true);
		b1.setBounds(50, 50, 130, 30);
		JRadioButton b2 = new JRadioButton("魔法 英雄");
		b2.setBounds(50, 100, 130, 30);

		// 按钮分组
		ButtonGroup bg = new ButtonGroup();
		// 把b1,b2放在 同一个 按钮分组对象里 ,这样同一时间,只有一个 按钮 会被选中
		bg.add(b1);
		bg.add(b2);

		f.add(b1);
		f.add(b2);
		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);
	}
}

下拉框

JComboBox 下拉框
使用getSelectedItem来获取被选中项
使用setSelectedItem() 来指定要选中项

package gui;

import javax.swing.JComboBox;
import javax.swing.JFrame;

public class TestGUI {
	public static void main(String[] args) {

		JFrame f = new JFrame("LoL");
		f.setSize(400, 300);
		f.setLocation(580, 240);
		f.setLayout(null);

		//下拉框出现的条目
		String[] heros = new String[] { "卡特琳娜", "库奇" };
		JComboBox cb = new JComboBox(heros);

		cb.setBounds(50, 50, 80, 30);

		f.add(cb);

		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);
	}
}

对话框

JOptionPane 用于弹出对话框

JOptionPane.showConfirmDialog(f, "是否 使用外挂 ?");
表示询问,第一个参数是该对话框以哪个组件对齐
JOptionPane.showInputDialog(f, "请输入yes,表明使用外挂后果自负");
接受用户的输入
JOptionPane.showMessageDialog(f, "你使用外挂被抓住! 罚拣肥皂3次!");
显示消息

package gui;

import javax.swing.JFrame;
import javax.swing.JOptionPane;

public class TestGUI {
	public static void main(String[] args) {

		JFrame f = new JFrame("LoL");
		f.setSize(400, 300);
		f.setLocation(580, 240);
		f.setLayout(null);

		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);

		int option = JOptionPane.showConfirmDialog(f, "是否 使用外挂 ?");
		if (JOptionPane.OK_OPTION == option) {
			String answer = JOptionPane.showInputDialog(f, "请输入yes,表明使用外挂后果自负");
			if ("yes".equals(answer))
				JOptionPane.showMessageDialog(f, "你使用外挂被抓住! 罚拣肥皂3次!");
		}

	}
}

文本框

JTextField 输入框
setText 设置文本
getText 获取文本
JTextField 是单行文本框,如果要输入多行数据,请使用JTextArea

tfPassword.grabFocus(); 表示让密码输入框获取焦点

package gui;

import java.awt.Dimension;
import java.awt.FlowLayout;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;

public class TestGUI {
	public static void main(String[] args) {

		JFrame f = new JFrame("LoL");
		f.setSize(400, 300);
		f.setLocation(200, 200);

		f.setLayout(new FlowLayout());

		JLabel lName = new JLabel("账号:");
		// 输入框
		JTextField tfName = new JTextField("");
		tfName.setText("请输入账号");
		tfName.setPreferredSize(new Dimension(80, 30));

		JLabel lPassword = new JLabel("密码:");
		// 输入框
		JTextField tfPassword = new JTextField("");
		tfPassword.setText("请输入密码");
		tfPassword.setPreferredSize(new Dimension(80, 30));

		f.add(lName);
		f.add(tfName);
		f.add(lPassword);
		f.add(tfPassword);

		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);
		tfPassword.grabFocus();
	}
}

 密码框

JPasswordField 密码框
与文本框不同,获取密码框里的内容,推荐使用getPassword,该方法会返回一个字符数组,而非字符串

package gui;

import java.awt.Dimension;
import java.awt.FlowLayout;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPasswordField;

public class TestGUI {
	public static void main(String[] args) {

		JFrame f = new JFrame("LoL");
		f.setSize(400, 300);
		f.setLocation(200, 200);

		f.setLayout(new FlowLayout());

		JLabel l = new JLabel("密码:");
		// 密码框
		JPasswordField pf = new JPasswordField("");
		pf.setText("&48kdh4@#");
		pf.setPreferredSize(new Dimension(80, 30));

		// 与文本框不同,获取密码框里的内容,推荐使用getPassword,该方法会返回一个字符数组,而非字符串
		char[] password = pf.getPassword();
		String p = String.valueOf(password);
		System.out.println(p);

		f.add(l);
		f.add(pf);

		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);
	}
}

文本域

JTextArea:文本域。
文本框JTextField不同的是,文本域可以输入多行数据
如果要给文本域初始文本,通过\n来实现换行效果
JTextArea通常会用到append来进行数据追加
如果文本太长,会跑出去,可以通过setLineWrap(true) 来做到自动换行

package gui;
 
import java.awt.Dimension;
import java.awt.FlowLayout;
 
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextArea;
 
public class TestGUI {
    public static void main(String[] args) {
 
        JFrame f = new JFrame("LoL");
        f.setSize(400, 300);
        f.setLocation(200, 200);
 
        f.setLayout(new FlowLayout());
 
        JLabel l = new JLabel("文本域:");
 
        JTextArea ta = new JTextArea();
        ta.setPreferredSize(new Dimension(200, 150));
        //\n换行符
        ta.setText("抢人头!\n抢你妹啊抢!\n");
        //追加数据
        ta.append("我去送了了了了了了了了了了了了了了了了了了了了了了了了");
        //设置自动换行
        ta.setLineWrap(true);
        f.add(l);
        f.add(ta);
 
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
        f.setVisible(true);
    }
}

进度条

package gui;

import java.awt.Dimension;
import java.awt.FlowLayout;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JProgressBar;
import javax.swing.JTextArea;

public class TestGUI {
	public static void main(String[] args) {

		JFrame f = new JFrame("LoL");
		f.setSize(400, 300);
		f.setLocation(200, 200);

		f.setLayout(new FlowLayout());

		JProgressBar pb = new JProgressBar();

		//进度条最大100
		pb.setMaximum(100);
		//当前进度是50
		pb.setValue(50);
		//显示当前进度
		pb.setStringPainted(true);

		f.add(pb);

		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);
	}
}

 文件选择器

JFileChooser 表示文件选择器
使用FileFilter用于仅选择.txt文件

fc.setFileFilter(new FileFilter() {

public String getDescription() {

return ".txt";

}

public boolean accept(File f) {

return f.getName().toLowerCase().endsWith(".txt");

}

});



fc.showOpenDialog(); 用于打开文件
fc.showSaveDialog(); 用于保存文件

package gui;
 
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
 
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.filechooser.FileFilter;
 
public class TestGUI {
 
    public static void main(String[] args) {
 
        JFrame f = new JFrame("LOL");
        f.setLayout(new FlowLayout());
        JFileChooser fc = new JFileChooser();
        fc.setFileFilter(new FileFilter() {
			
			@Override
			public String getDescription() {
				// TODO Auto-generated method stub
				return ".txt";
			}
			
			@Override
			public boolean accept(File f) {
				return f.getName().toLowerCase().endsWith(".txt");
			}
		});
 
        JButton bOpen = new JButton("打开文件");
 
        JButton bSave = new JButton("保存文件");
 
        f.add(bOpen);
        f.add(bSave);
 
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setSize(250, 150);
        f.setLocationRelativeTo(null);
 
        f.setVisible(true);
         
        bOpen.addActionListener(new ActionListener() {
             
            @Override
            public void actionPerformed(ActionEvent e) {
                 int returnVal =  fc.showOpenDialog(f);
                 File file = fc.getSelectedFile();
                 if (returnVal == JFileChooser.APPROVE_OPTION) {
                     JOptionPane.showMessageDialog(f, "计划打开文件:" + file.getAbsolutePath());
                 }
                 
            }
        });
        bSave.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                int returnVal =  fc.showSaveDialog(f);
                File file = fc.getSelectedFile();
                if (returnVal == JFileChooser.APPROVE_OPTION) {
                    JOptionPane.showMessageDialog(f, "计划保存到文件:" + file.getAbsolutePath());
                }
            }
        });
 
    }
 
}

 6、面板

基本面板

JPanel即为基本面板
面板和JFrame一样都是容器,不过面板一般用来充当中间容器,把组件放在面板上,然后再把面板放在窗体上。
一旦移动一个面板,其上面的组件,就会全部统一跟着移动,采用这种方式,便于进行整体界面的设计

package gui;

import java.awt.Color;
import java.awt.FlowLayout;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;

public class TestGUI {
	public static void main(String[] args) {

		JFrame f = new JFrame("LoL");
		f.setSize(400, 300);
		f.setLocation(200, 200);

		f.setLayout(null);

		JPanel p1 = new JPanel();
		// 设置面板大小
		p1.setBounds(50, 50, 300, 60);
		// 设置面板背景颜色
		p1.setBackground(Color.RED);

		// 这一句可以没有,因为JPanel默认就是采用的FlowLayout
		p1.setLayout(new FlowLayout());

		JButton b1 = new JButton("英雄1");
		JButton b2 = new JButton("英雄2");
		JButton b3 = new JButton("英雄3");

		// 把按钮加入面板
		p1.add(b1);
		p1.add(b2);
		p1.add(b3);

		JPanel p2 = new JPanel();
		JButton b4 = new JButton("英雄4");
		JButton b5 = new JButton("英雄5");
		JButton b6 = new JButton("英雄6");

		p2.add(b4);
		p2.add(b5);
		p2.add(b6);

		p2.setBackground(Color.BLUE);
		p2.setBounds(10, 150, 300, 60);

		// 把面板加入窗口
		f.add(p1);
		f.add(p2);

		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);
	}
}

 ContentPane

JFrame上有一层面板,叫做ContentPane
平时通过f.add()向JFrame增加组件,其实是向JFrame上的 ContentPane加东西

package gui;

import javax.swing.JButton;
import javax.swing.JFrame;

public class TestGUI {
	public static void main(String[] args) {

		JFrame f = new JFrame("LoL");
		f.setSize(400, 300);
		f.setLocation(200, 200);
		f.setLayout(null);
		JButton b = new JButton("一键秒对方基地挂");
		b.setBounds(50, 50, 280, 30);

		f.add(b);
		// JFrame上有一层面板,叫做ContentPane
		// 平时通过f.add()向JFrame增加组件,其实是向JFrame上的 ContentPane加东西
		// f.add等同于f.getContentPane().add(b);
		f.getContentPane().add(b);

		// b.getParent()获取按钮b所处于的容器
		// 打印出来可以看到,实际上是ContentPane而非JFrame
		System.out.println(b.getParent());

		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);
	}
}

SplitPanel

创建一个水平JSplitPane,左边是pLeft,右边是pRight

package gui;
 
import java.awt.Color;
import java.awt.FlowLayout;
 
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JSplitPane;
 
public class TestGUI {
    public static void main(String[] args) {
 
        JFrame f = new JFrame("LoL");
        f.setSize(400, 300);
        f.setLocation(200, 200);
 
        f.setLayout(null);
 
        JPanel pLeft = new JPanel();
        pLeft.setBounds(50, 50, 300, 60);
 
        pLeft.setBackground(Color.RED);
 
        pLeft.setLayout(new FlowLayout());
 
        JButton b1 = new JButton("盖伦");
        JButton b2 = new JButton("提莫");
        JButton b3 = new JButton("安妮");
 
        pLeft.add(b1);
        pLeft.add(b2);
        pLeft.add(b3);
 
        JPanel pRight = new JPanel();
        JButton b4 = new JButton("英雄4");
        JButton b5 = new JButton("英雄5");
        JButton b6 = new JButton("英雄6");
 
        pRight.add(b4);
        pRight.add(b5);
        pRight.add(b6);
 
        pRight.setBackground(Color.BLUE);
        pRight.setBounds(10, 150, 300, 60);
 
        // 创建一个水平JSplitPane,左边是p1,右边是p2
        JSplitPane sp = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, pLeft, pRight);
        // 设置分割条的位置
        sp.setDividerLocation(80);
 
        // 把sp当作ContentPane
        f.setContentPane(sp);
 
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
        f.setVisible(true);
    }
}

 JScrollPanel

使用带滚动条的面板有两种方式
1. 在创建JScrollPane,把组件作为参数传进去

JScrollPane sp = new JScrollPane(ta);


2. 希望带滚动条的面板显示其他组件的时候,调用setViewportView

sp.setViewportView(ta);

package gui;

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;

public class TestGUI {
	public static void main(String[] args) {

		JFrame f = new JFrame("LoL");
		f.setSize(400, 300);
		f.setLocation(200, 200);

		f.setLayout(null);
		//准备一个文本域,在里面放很多数据
		JTextArea ta = new JTextArea();
		for (int i = 0; i < 1000; i++) {
			ta.append(String.valueOf(i));
		}
		//自动换行
		ta.setLineWrap(true);
		JScrollPane sp = new JScrollPane(ta);

		f.setContentPane(sp);

		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);
	}
}

 TabbedPanel

package gui;
 
import java.awt.Color;
import java.awt.FlowLayout;

import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
 
public class TestGUI {
    public static void main(String[] args) {
 
        JFrame f = new JFrame("LoL");
        f.setSize(400, 300);
        f.setLocation(200, 200);
 
        f.setLayout(null);
 
        JPanel p1 = new JPanel();
        p1.setBounds(50, 50, 300, 60);
 
        p1.setBackground(Color.RED);
 
        p1.setLayout(new FlowLayout());
 
        JButton b1 = new JButton("英雄1");
        JButton b2 = new JButton("英雄2");
        JButton b3 = new JButton("英雄3");
 
        p1.add(b1);
        p1.add(b2);
        p1.add(b3);
 
        JPanel p2 = new JPanel();
        JButton b4 = new JButton("英雄4");
        JButton b5 = new JButton("英雄5");
        JButton b6 = new JButton("英雄6");
 
        p2.add(b4);
        p2.add(b5);
        p2.add(b6);
 
        p2.setBackground(Color.BLUE);
        p2.setBounds(10, 150, 300, 60);
 
        JTabbedPane tp = new JTabbedPane();
        tp.add(p1);
        tp.add(p2);
 
        // 设置tab的标题
        tp.setTitleAt(0, "红色tab");
        tp.setTitleAt(1, "蓝色tab");
        
        ImageIcon i = new ImageIcon("e:/project/j2se/j.png");
        tp.setIconAt(0,i );
        tp.setIconAt(1,i );
 
        f.setContentPane(tp);
 
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
        f.setVisible(true);
    }
}

 CardLayerout

CardLayerout 布局器 很像TabbedPanel ,在本例里面上面是一个下拉框,下面是一个CardLayerout 的JPanel
这个JPanel里有两个面板,可以通过CardLayerout方便的切换

package gui;

import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;

import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;

public class TestGUI {

	public static void main(String[] args) {
		JFrame f = new JFrame("CardLayerout");

		JPanel comboBoxPane = new JPanel();
		String buttonPanel = "按钮面板";
		String inputPanel = "输入框面板";
		String comboBoxItems[] = { buttonPanel, inputPanel };
		JComboBox<String> cb = new JComboBox<>(comboBoxItems);
		comboBoxPane.add(cb);

		// 两个Panel充当卡片
		JPanel card1 = new JPanel();
		card1.add(new JButton("按钮 1"));
		card1.add(new JButton("按钮 2"));
		card1.add(new JButton("按钮 3"));

		JPanel card2 = new JPanel();
		card2.add(new JTextField("输入框", 20));

		JPanel cards; // a panel that uses CardLayout
		cards = new JPanel(new CardLayout());
		cards.add(card1, buttonPanel);
		cards.add(card2, inputPanel);

		f.add(comboBoxPane, BorderLayout.NORTH);
		f.add(cards, BorderLayout.CENTER);

		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		f.setSize(250, 150);
		f.setLocationRelativeTo(null);
		f.setVisible(true);

		cb.addItemListener(new ItemListener() {

			@Override
			public void itemStateChanged(ItemEvent evt) {
				CardLayout cl = (CardLayout) (cards.getLayout());
				cl.show(cards, (String) evt.getItem());
			}
		});		
	}
		
}

7、菜单

GUI的菜单分为 菜单栏,菜单和菜单项

菜单栏和菜单

package gui;

import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;

public class TestGUI {
	public static void main(String[] args) {

		JFrame f = new JFrame("LoL");
		f.setSize(400, 300);
		f.setLocation(200, 200);

		// 菜单栏
		JMenuBar mb = new JMenuBar();

		// 菜单
		JMenu mHero = new JMenu("英雄");
		JMenu mItem = new JMenu("道具");
		JMenu mWord = new JMenu("符文");
		JMenu mSummon = new JMenu("召唤师");
		JMenu mTalent = new JMenu("天赋树");

		// 把菜单加入到菜单栏
		mb.add(mHero);
		mb.add(mItem);
		mb.add(mWord);
		mb.add(mSummon);
		mb.add(mTalent);

		// 把菜单栏加入到frame,这里用的是set而非add
		f.setJMenuBar(mb);

		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);
	}
}

 菜单项

package gui;

import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;

public class TestGUI {
	public static void main(String[] args) {

		JFrame f = new JFrame("LoL");
		f.setSize(400, 400);
		f.setLocation(200, 200);

		JMenuBar mb = new JMenuBar();

		JMenu mHero = new JMenu("英雄");
		JMenu mItem = new JMenu("道具");
		JMenu mWord = new JMenu("符文");
		JMenu mSummon = new JMenu("召唤师");
		JMenu mTalent = new JMenu("天赋树");

		// 菜单项
		mHero.add(new JMenuItem("近战-Warriar"));
		mHero.add(new JMenuItem("远程-Range"));
		mHero.add(new JMenuItem("物理-physical"));
		mHero.add(new JMenuItem("坦克-Tank"));
		mHero.add(new JMenuItem("法系-Mage"));
		mHero.add(new JMenuItem("辅助-Support"));
		mHero.add(new JMenuItem("打野-Jungle"));
		mHero.add(new JMenuItem("突进-Charge"));
		mHero.add(new JMenuItem("男性-Boy"));
		mHero.add(new JMenuItem("女性-Girl"));
		// 分隔符 
		mHero.addSeparator();
		mHero.add(new JMenuItem("所有-All"));

		mb.add(mHero);
		mb.add(mItem);
		mb.add(mWord);
		mb.add(mSummon);
		mb.add(mTalent);

		f.setJMenuBar(mb);

		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);
	}
}

8、工具栏

工具栏用于存放常用的按钮

工具栏

package gui;

import java.awt.BorderLayout;

import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JToolBar;

public class TestGUI {
	public static void main(String[] args) {

		JFrame f = new JFrame("LoL");
		f.setSize(400, 300);
		f.setLocation(200, 200);
		// 菜单
		addMenu(f);

		// 工具栏
		JToolBar tb = new JToolBar();
		// 为工具栏增加按钮
		JButton b1 = new JButton(new ImageIcon("e:/project/j2se/1.jpg"));
		JButton b2 = new JButton(new ImageIcon("e:/project/j2se/2.jpg"));
		JButton b3 = new JButton(new ImageIcon("e:/project/j2se/3.jpg"));
		JButton b4 = new JButton(new ImageIcon("e:/project/j2se/4.jpg"));
		JButton b5 = new JButton(new ImageIcon("e:/project/j2se/5.jpg"));
		JButton b6 = new JButton(new ImageIcon("e:/project/j2se/6.jpg"));
		tb.add(b1);
		tb.add(b2);
		tb.add(b3);
		tb.add(b4);
		tb.add(b5);
		tb.add(b6);

		// 把工具栏放在north的位置
		f.setLayout(new BorderLayout());
		f.add(tb, BorderLayout.NORTH);

		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);
	}

	private static void addMenu(JFrame f) {
		JMenuBar mb = new JMenuBar();

		JMenu mHero = new JMenu("英雄");
		JMenu mItem = new JMenu("道具");
		JMenu mWord = new JMenu("符文");
		JMenu mSummon = new JMenu("召唤师");
		JMenu mTalent = new JMenu("天赋树");

		// 菜单项
		mHero.add(new JMenuItem("近战-Warriar"));
		mHero.add(new JMenuItem("远程-Range"));
		mHero.add(new JMenuItem("物理-physical"));
		mHero.add(new JMenuItem("坦克-Tank"));
		mHero.add(new JMenuItem("法系-Mage"));
		mHero.add(new JMenuItem("辅助-Support"));
		mHero.add(new JMenuItem("打野-Jungle"));
		mHero.add(new JMenuItem("突进-Charge"));
		mHero.add(new JMenuItem("男性-Boy"));
		mHero.add(new JMenuItem("女性-Girl"));

		mb.add(mHero);
		mb.add(mItem);
		mb.add(mWord);
		mb.add(mSummon);
		mb.add(mTalent);

		f.setJMenuBar(mb);
	}
}

给按钮设置提示信息

当鼠标放在按钮上的时候会出现提示

package gui;

import java.awt.BorderLayout;

import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JToolBar;

public class TestGUI {
	public static void main(String[] args) {

		JFrame f = new JFrame("LoL");
		f.setSize(400, 300);
		f.setLocation(200, 200);
		addMenu(f);

		JToolBar tb = new JToolBar();
		JButton b1 = new JButton(new ImageIcon("e:/project/j2se/1.jpg"));
		JButton b2 = new JButton(new ImageIcon("e:/project/j2se/2.jpg"));
		JButton b3 = new JButton(new ImageIcon("e:/project/j2se/3.jpg"));
		JButton b4 = new JButton(new ImageIcon("e:/project/j2se/4.jpg"));
		JButton b5 = new JButton(new ImageIcon("e:/project/j2se/5.jpg"));
		JButton b6 = new JButton(new ImageIcon("e:/project/j2se/6.jpg"));
		tb.add(b1);
		tb.add(b2);
		tb.add(b3);
		tb.add(b4);
		tb.add(b5);
		tb.add(b6);

		// 给按钮设置提示信息
		b1.setToolTipText("坑爹英雄");

		// 把工具栏放在north的位置
		f.setLayout(new BorderLayout());
		f.add(tb, BorderLayout.NORTH);

		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);
	}

	private static void addMenu(JFrame f) {
		JMenuBar mb = new JMenuBar();

		JMenu mHero = new JMenu("英雄");
		JMenu mItem = new JMenu("道具");
		JMenu mWord = new JMenu("符文");
		JMenu mSummon = new JMenu("召唤师");
		JMenu mTalent = new JMenu("天赋树");

		// 菜单项
		mHero.add(new JMenuItem("近战-Warriar"));
		mHero.add(new JMenuItem("远程-Range"));
		mHero.add(new JMenuItem("物理-physical"));
		mHero.add(new JMenuItem("坦克-Tank"));
		mHero.add(new JMenuItem("法系-Mage"));
		mHero.add(new JMenuItem("辅助-Support"));
		mHero.add(new JMenuItem("打野-Jungle"));
		mHero.add(new JMenuItem("突进-Charge"));
		mHero.add(new JMenuItem("男性-Boy"));
		mHero.add(new JMenuItem("女性-Girl"));

		mb.add(mHero);
		mb.add(mItem);
		mb.add(mWord);
		mb.add(mSummon);
		mb.add(mTalent);

		f.setJMenuBar(mb);
	}
}

禁止工具栏拖动

默认情况下 工具栏可以通过鼠标拖动

setFloatable(false);


可以禁止鼠标拖动功能

package gui;

import java.awt.BorderLayout;

import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JToolBar;

public class TestGUI {
	public static void main(String[] args) {

		JFrame f = new JFrame("LoL");
		f.setSize(400, 300);
		f.setLocation(200, 200);
		addMenu(f);

		JToolBar tb = new JToolBar();
		JButton b1 = new JButton(new ImageIcon("e:/project/j2se/1.jpg"));
		JButton b2 = new JButton(new ImageIcon("e:/project/j2se/2.jpg"));
		JButton b3 = new JButton(new ImageIcon("e:/project/j2se/3.jpg"));
		JButton b4 = new JButton(new ImageIcon("e:/project/j2se/4.jpg"));
		JButton b5 = new JButton(new ImageIcon("e:/project/j2se/5.jpg"));
		JButton b6 = new JButton(new ImageIcon("e:/project/j2se/6.jpg"));
		tb.add(b1);
		tb.add(b2);
		tb.add(b3);
		tb.add(b4);
		tb.add(b5);
		tb.add(b6);

		b1.setToolTipText("坑爹英雄");

		// 禁止工具栏拖动
		tb.setFloatable(false);

		// 把工具栏放在north的位置
		f.setLayout(new BorderLayout());
		f.add(tb, BorderLayout.NORTH);

		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);
	}

	private static void addMenu(JFrame f) {
		JMenuBar mb = new JMenuBar();

		JMenu mHero = new JMenu("英雄");
		JMenu mItem = new JMenu("道具");
		JMenu mWord = new JMenu("符文");
		JMenu mSummon = new JMenu("召唤师");
		JMenu mTalent = new JMenu("天赋树");

		// 菜单项
		mHero.add(new JMenuItem("近战-Warriar"));
		mHero.add(new JMenuItem("远程-Range"));
		mHero.add(new JMenuItem("物理-physical"));
		mHero.add(new JMenuItem("坦克-Tank"));
		mHero.add(new JMenuItem("法系-Mage"));
		mHero.add(new JMenuItem("辅助-Support"));
		mHero.add(new JMenuItem("打野-Jungle"));
		mHero.add(new JMenuItem("突进-Charge"));
		mHero.add(new JMenuItem("男性-Boy"));
		mHero.add(new JMenuItem("女性-Girl"));

		mb.add(mHero);
		mb.add(mItem);
		mb.add(mWord);
		mb.add(mSummon);
		mb.add(mTalent);

		f.setJMenuBar(mb);
	}
}

9、表格

基本表格

显示一个Table需要两组数据
1. 一维数组: String[]columnNames 表示表格的标题
2. 二维数组: String[][] heros 表格中的内容
默认情况下,表格的标题是不会显示出来了,除非使用了JScrollPane

package gui;

import java.awt.BorderLayout;

import javax.swing.JFrame;
import javax.swing.JTable;

public class TestGUI {
	public static void main(String[] args) {

		JFrame f = new JFrame("LoL");
		f.setSize(400, 300);
		f.setLocation(200, 200);
		f.setLayout(new BorderLayout());

		// 表格上的title
		String[] columnNames = new String[] { "id", "name", "hp", "damage" };
		// 表格中的内容,是一个二维数组
		String[][] heros = new String[][] { { "1", "盖伦", "616", "100" },
				{ "2", "提莫", "512", "102" }, { "3", "奎因", "832", "200" } };
		JTable t = new JTable(heros, columnNames);
		f.add(t, BorderLayout.CENTER);

		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);
	}
}

JScrollPane

JScrollPane: 带滚动条的Panel
把table放进去就可以看到table的title
同样的把textarea放进去,并且textarea内容够长的话,就会看到滚动条

package gui;

import java.awt.BorderLayout;

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;

public class TestGUI {
	public static void main(String[] args) {

		JFrame f = new JFrame("LoL");
		f.setSize(400, 300);
		f.setLocation(200, 200);
		f.setLayout(new BorderLayout());

		String[] columnNames = new String[] { "id", "name", "hp", "damage" };
		String[][] heros = new String[][] { { "1", "盖伦", "616", "100" },
				{ "2", "提莫", "512", "102" }, { "3", "奎因", "832", "200" } };
		JTable t = new JTable(heros, columnNames);

		// 根据t创建 JScrollPane
		JScrollPane sp = new JScrollPane(t);

		//或则创建一个空的JScrollPane,再通过setViewportView把table放在JScrollPane中
		// JScrollPane sp = new JScrollPane(t);
		// sp.setViewportView(t);

		// 把sp而非JTable加入到JFrame上,
		f.add(sp, BorderLayout.CENTER);

		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);
	}
}

列宽

设置列宽度

package gui;

import java.awt.BorderLayout;

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;

public class TestGUI {
	public static void main(String[] args) {

		JFrame f = new JFrame("LoL");
		f.setSize(400, 300);
		f.setLocation(200, 200);
		f.setLayout(new BorderLayout());

		String[] columnNames = new String[] { "id", "name", "hp", "damage" };
		String[][] heros = new String[][] { { "1", "盖伦", "616", "100" },
				{ "2", "提莫", "512", "102" }, { "3", "奎因", "832", "200" } };
		JTable t = new JTable(heros, columnNames);

		JScrollPane sp = new JScrollPane(t);

		// 设置列宽度
		t.getColumnModel().getColumn(0).setPreferredWidth(10);

		f.add(sp, BorderLayout.CENTER);

		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);
	}
}

TableModel

首先说下TableModel的设计思想,在Model这种思想的指导下,数据和显示分离开来了。 比如对于JTable而言,有数据部分,也有显示部分(比如列宽等信息)。 数据部分,专门做一个类,叫做TableModel,就用于存放要显示的数据。

使用TableModel的方式存放Table需要显示的数据
HeroTableModel 继承AbstractTableModel ,进而实现了接口TableModel
在HeroTableModel 中提供一个table显示需要的所有信息
1. getRowCount 返回一共有多少行
2. getColumnCount 返回一共有多少列
3. getColumnName 每一列的名字
4. isCellEditable 单元格是否可以修改
5. getValueAt 每一个单元格里的值

当图形界面需要渲染第一个单元格的数据的时候,就会调用方法TabelModel的getValueAt(0,0) ,把返回值拿到并显示

package gui;

import javax.swing.table.AbstractTableModel;

public class HeroTableModel extends AbstractTableModel {

	String[] columnNames = new String[] { "id", "name", "hp", "damage" };
	String[][] heros = new String[][] { { "1", "盖伦", "616", "100" },
			{ "2", "提莫", "512", "102" }, { "3", "奎因", "832", "200" } };

	// 返回一共有多少行
	public int getRowCount() {
		// TODO Auto-generated method stub
		return heros.length;
	}

	// 返回一共有多少列
	public int getColumnCount() {
		// TODO Auto-generated method stub
		return columnNames.length;
	}

	// 获取每一列的名称
	public String getColumnName(int columnIndex) {
		// TODO Auto-generated method stub
		return columnNames[columnIndex];
	}

	// 单元格是否可以修改
	public boolean isCellEditable(int rowIndex, int columnIndex) {
		return false;
	}

	// 每一个单元格里的值
	public Object getValueAt(int rowIndex, int columnIndex) {
		// TODO Auto-generated method stub
		return heros[rowIndex][columnIndex];
	}

}

进一步理解TableModel

在使用TableModel之前,是使用

String[] columnNames =。。。

String[][] heros = 。。。

JTable t = new JTable(heros, columnNames);


这样的风格创建一个JTable的
所以实际上调用的是如下的构造方法:

JTable(Object[][] rowData, Object[] columnNames)


如图所示,在JTable的的源代码中,它就会根据rowData和columnNames去创建一个TableModel对象

TableModel 与 DAO结合

通过TableModel与DAO结合显示数据库中Hero信息。
DAO使用HeroDAO
在TableModel中,使用从DAO返回的List作为TableModel的数据

只需要修改HeroTableModel,无需修改TestGUI。 这正好演绎了Model设计思想中的数据分离的好处,当只需要数据发生变化的时候,修改Model即可,界面GUI部分,不需要做任何改动

package gui;

import java.util.List;

import javax.swing.table.AbstractTableModel;

import jdbc.HeroDAO;
import charactor.Hero;

public class HeroTableModel extends AbstractTableModel {

	String[] columnNames = new String[] { "id", "name", "hp", "damage" };

	// 使用从DAO返回的List作为TableModel的数据

	public List<Hero> heros = new HeroDAO().list();

	// heros.size返回一共有多少行
	public int getRowCount() {
		// TODO Auto-generated method stub
		return heros.size();
	}

	public int getColumnCount() {
		// TODO Auto-generated method stub
		return columnNames.length;
	}

	public String getColumnName(int columnIndex) {
		// TODO Auto-generated method stub
		return columnNames[columnIndex];
	}

	public boolean isCellEditable(int rowIndex, int columnIndex) {
		return false;
	}

	// 先通过heros.get(rowIndex)获取行对应的Hero对象
	// 然后根据columnIndex返回对应的属性
	public Object getValueAt(int rowIndex, int columnIndex) {
		Hero h = heros.get(rowIndex);
		if (0 == columnIndex)
			return h.id;
		if (1 == columnIndex)
			return h.name;
		if (2 == columnIndex)
			return h.hp;
		if (3 == columnIndex)
			return h.damage;
		return null;
	}

}

TableSelectionModel

通过table可以获取一个 TableSelectionModel,专门用于监听jtable选中项的变化

package gui;
 
import java.awt.BorderLayout;
 
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
 
import charactor.Hero;
 
public class TestGUI {
    public static void main(String[] args) {
 
        JFrame f = new JFrame("LoL");
        f.setSize(400, 300);
        f.setLocation(200, 200);
        f.setLayout(new BorderLayout());
 
        final HeroTableModel htm = new HeroTableModel();
 
        final JTable t = new JTable(htm);
        // 准备一个Panel上面放一个Label用于显示哪条被选中了
        JPanel p = new JPanel();
        final JLabel l = new JLabel("暂时未选中条目");
        p.add(l);
 
        JScrollPane sp = new JScrollPane(t);
 
        // 使用selection监听器来监听table的哪个条目被选中
        t.getSelectionModel().addListSelectionListener(
                new ListSelectionListener() {
 
                    // 当选择了某一行的时候触发该事件
                    public void valueChanged(ListSelectionEvent e) {
                        // 获取哪一行被选中了
                        int row = t.getSelectedRow();
                        // 根据选中的行,到HeroTableModel中获取对应的对象
                        Hero h = htm.heros.get(row);
                        // 更新标签内容
                        l.setText("当前选中的英雄是: " + h.name);
 
                    }
                });
 
        f.add(p, BorderLayout.NORTH);
        f.add(sp, BorderLayout.CENTER);
 
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
        f.setVisible(true);
    }
}

更新Table

以新增数据到数据库中,然后更新Table为例

package gui;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;

import jdbc.HeroDAO;
import charactor.Hero;

public class TestGUI {
	public static void main(String[] args) {

		JFrame f = new JFrame("LoL");
		f.setSize(400, 300);
		f.setLocation(200, 200);
		f.setLayout(new BorderLayout());

		final HeroTableModel htm = new HeroTableModel();

		final JTable t = new JTable(htm);
		// 增加 一个 panel用于放置名称,血量输入框和增加 按钮
		JPanel p = new JPanel();

		final JLabel lName = new JLabel("名称");
		final JTextField tfName = new JTextField("");
		final JLabel lHp = new JLabel("血量");
		final JTextField tfHp = new JTextField("");
		JButton bAdd = new JButton("增加");
		tfName.setPreferredSize(new Dimension(80, 30));
		tfHp.setPreferredSize(new Dimension(80, 30));

		p.add(lName);
		p.add(tfName);
		p.add(lHp);
		p.add(tfHp);
		p.add(bAdd);

		// 为增加按钮添加监听
		bAdd.addActionListener(new ActionListener() {

			@Override
			public void actionPerformed(ActionEvent e) {

				HeroDAO dao = new HeroDAO();

				// 根据输入框数据创建一个Hero对象
				Hero h = new Hero();
				h.name = tfName.getText();
				h.hp = Integer.parseInt(tfHp.getText());

				// 通过dao把该对象加入到数据库
				dao.add(h);

				// 通过dao更新tablemodel中的数据
				htm.heros = dao.list();
				// 调用JTable的updateUI,刷新界面。
				// 刷新界面的时候,会到tablemodel中去取最新的数据
				// 就能看到新加进去的数据了

				t.updateUI();
			}
		});

		JScrollPane sp = new JScrollPane(t);

		f.add(p, BorderLayout.NORTH);
		f.add(sp, BorderLayout.CENTER);

		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);
	}
}

输入项验证

如果用户输入的名称为空,或者血量不是小数,在提交数据的时候都会报错。
“感觉上” 界面就卡住了。 这是不友好的人机交互行为。
所以需要加上输入项的验证,如果输入的数据不合格,应该弹出对话框提示用户具体原因。

package gui;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;

import jdbc.HeroDAO;
import charactor.Hero;

public class TestGUI {
	public static void main(String[] args) {

		final JFrame f = new JFrame("LoL");
		f.setSize(400, 300);
		f.setLocation(200, 200);
		f.setLayout(new BorderLayout());

		final HeroTableModel htm = new HeroTableModel();

		final JTable t = new JTable(htm);
		JPanel p = new JPanel();

		final JLabel lName = new JLabel("名称");
		final JTextField tfName = new JTextField("");
		final JLabel lHp = new JLabel("血量");
		final JTextField tfHp = new JTextField("");
		JButton bAdd = new JButton("增加");
		tfName.setPreferredSize(new Dimension(80, 30));
		tfHp.setPreferredSize(new Dimension(80, 30));

		p.add(lName);
		p.add(tfName);
		p.add(lHp);
		p.add(tfHp);
		p.add(bAdd);

		bAdd.addActionListener(new ActionListener() {

			@Override
			public void actionPerformed(ActionEvent e) {

				HeroDAO dao = new HeroDAO();

				Hero h = new Hero();
				String name = tfName.getText();

				// 通过name长度判断 名称是否为空
				if (name.length() == 0) {
					// 弹出对话框提示用户
					JOptionPane.showMessageDialog(f, "名称不能为空");

					// 名称输入框获取焦点
					tfName.grabFocus();
					return;
				}

				String hp = tfHp.getText().trim();

				try {
					// 把hp转换为浮点型,如果出现异常NumberFormatException表示不是浮点型格式
					Float.parseFloat(hp);
				} catch (NumberFormatException e1) {
					JOptionPane.showMessageDialog(f, "血量只能是小数 ");
					tfHp.grabFocus();
					return;
				}

				h.name = name;

				h.hp = Float.parseFloat(hp);

				dao.add(h);

				htm.heros = dao.list();

				t.updateUI();

			}
		});

		JScrollPane sp = new JScrollPane(t);

		f.add(p, BorderLayout.NORTH);
		f.add(sp, BorderLayout.CENTER);

		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);
	}
}

选中指定行

1. table初始化后,应该默认选中第一行
2. 增加数据后,也应该选中新增的这一条

package gui;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;

import jdbc.HeroDAO;
import charactor.Hero;

public class TestGUI {
	public static void main(String[] args) {

		final JFrame f = new JFrame("LoL");
		f.setSize(400, 300);
		f.setLocation(200, 200);
		f.setLayout(new BorderLayout());

		final HeroTableModel htm = new HeroTableModel();

		final JTable t = new JTable(htm);
		// 设置选择模式为 只能选中一行
		t.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
		// 选中第一行 (基本0)
		t.getSelectionModel().setSelectionInterval(0, 0);

		JPanel p = new JPanel();

		final JLabel lName = new JLabel("名称");
		final JTextField tfName = new JTextField("");
		final JLabel lHp = new JLabel("血量");
		final JTextField tfHp = new JTextField("");
		JButton bAdd = new JButton("增加");
		tfName.setPreferredSize(new Dimension(80, 30));
		tfHp.setPreferredSize(new Dimension(80, 30));

		p.add(lName);
		p.add(tfName);
		p.add(lHp);
		p.add(tfHp);
		p.add(bAdd);

		bAdd.addActionListener(new ActionListener() {

			@Override
			public void actionPerformed(ActionEvent e) {

				HeroDAO dao = new HeroDAO();

				Hero h = new Hero();
				String name = tfName.getText();

				if (name.length() == 0) {

					JOptionPane.showMessageDialog(f, "名称不能为空");

					tfName.grabFocus();
					return;
				}

				String hp = tfHp.getText().trim();

				try {

					Float.parseFloat(hp);
				} catch (NumberFormatException e1) {
					JOptionPane.showMessageDialog(f, "血量只能是小数 ");
					tfHp.grabFocus();
					return;
				}

				h.name = name;

				h.hp = Float.parseFloat(hp);

				dao.add(h);

				htm.heros = dao.list();

				t.updateUI();

				// 选中 第一行 ,因为 DAO是按照 ID倒排序查询,所以第一行就是新加入的数据
				t.getSelectionModel().setSelectionInterval(0, 0);
			}
		});

		JScrollPane sp = new JScrollPane(t);

		f.add(p, BorderLayout.NORTH);
		f.add(sp, BorderLayout.CENTER);

		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);
	}
}

10、日期控件

swing没有自带的日期控件,需要第三方的类
jar包可以在右侧下载

DatePicker

本例使用 datepicker.jar 包,有一个缺点,不能设置时间,只能在创建控件的时候传入指定日期。
需要设置日期,请使用JXDatePicker

package gui;
  
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Date;
import java.util.Locale;
  
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
 
import com.eltima.components.ui.DatePicker;
  
public class TestGUI {
	public static void main(String[] args) {
  
        JFrame f = new JFrame("LoL");
        f.setSize(400, 300);
        f.setLocation(200, 200);
        f.setLayout(null);
  
        final DatePicker datepick;
        datepick = getDatePicker();
  
        f.add(datepick);
  
        JButton b = new JButton("获取时间");
        b.setBounds(137, 183, 100, 30);
        f.add(b);
  
        b.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                JOptionPane.showMessageDialog(f, "获取控件中的日期:" + datepick.getValue());
                System.out.println(datepick.getValue());
            }
        });
  
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  
        f.setVisible(true);
    }

	private static DatePicker getDatePicker() {
		final DatePicker datepick;
		// 格式
        String DefaultFormat = "yyyy-MM-dd HH:mm:ss";
        // 当前时间
        Date date = new Date();
        // 字体
        Font font = new Font("Times New Roman", Font.BOLD, 14);
  
        Dimension dimension = new Dimension(177, 24);
  
        int[] hilightDays = { 1, 3, 5, 7 };
  
        int[] disabledDays = { 4, 6, 5, 9 };
  
        datepick = new DatePicker(date, DefaultFormat, font, dimension);
  
        datepick.setLocation(137, 83);
        datepick.setBounds(137, 83, 177, 24);
        // 设置一个月份中需要高亮显示的日子
        datepick.setHightlightdays(hilightDays, Color.red);
        // 设置一个月份中不需要的日子,呈灰色显示
        datepick.setDisableddays(disabledDays);
        // 设置国家
        datepick.setLocale(Locale.CHINA);
        // 设置时钟面板可见
        datepick.setTimePanleVisible(true);
		return datepick;
	}
}

JXDatePicker

本例使用 包swingx-core-1.6.2.jar,界面比较简约,可以设置日期

package gui;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Date;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;

import org.jdesktop.swingx.JXDatePicker;

public class TestGUI {
	public static void main(String[] args) {

		JFrame f = new JFrame("LoL");
		f.setSize(400, 300);
		f.setLocation(200, 200);
		f.setLayout(null);

		Date date = new Date();

		final JXDatePicker datepick = new JXDatePicker();

		// 设置 date日期
		datepick.setDate(date);

		datepick.setBounds(137, 83, 177, 24);

		f.add(datepick);

		JButton b = new JButton("获取时间");
		b.setBounds(137, 183, 100, 30);
		f.add(b);

		b.addActionListener(new ActionListener() {

			@Override
			public void actionPerformed(ActionEvent e) {
				// 获取 日期
				Date d = datepick.getDate();
				JOptionPane.showMessageDialog(f, "获取控件中的日期 :" + d);

			}
		});

		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		f.setVisible(true);
	}
}

11、Swing中的线程

三种线程

在Swing程序的开发中,需要建立3种线程的概念
1. 初始化线程
初始化线程用于创建各种容器组件并显示他们,一旦创建并显示,初始化线程的任务就结束了。

2. 事件调度线程
通过事件监听的学习,我们了解到Swing是一个事件驱动的模型,所有和事件相关的操作都放是放在事件调度线程 (Event Dispatch)中进行的。比如点击一个按钮,对应的ActionListener.actionPerformed 方法中的代码,就是在事件调度线程 Event Dispatch Thread中执行的。

3. 长耗时任务线程
有时候需要进行一些长时间的操作,比如访问数据库,文件复制,连接网络,统计文件总数等等。 这些操作就不适合放在事件调度线程中进行,因为占用时间久了,会让使用者感觉界面响应很卡顿。 为了保持界面响应的流畅性,所有长耗时任务都应该放在专门的 长耗时任务线程中进行

事件调度线程是单线程的

在开始讲解这3种线程之前, 要建立一个概念: 事件调度线程是单线程的。
为什么呢?
这是因为 Swing里面的各种组件类,比如JTextField,JButton 都不是线程安全的,这就意味着,如果有多个线程,那么同一个JTextField的setText方法,可能会被多个线程同时调用,这会导致同步问题以及错误数据的发生。

如果把组件类设计成为线程安全的,由于Swing事件调度的复杂性,就很有可能导致死锁的发生。

为了规避同步问题,以及降低整个Swing设计的复杂度,提高Swing的相应速度,Swing中的 事件调度线程被设计成为了单线程模式,即只有一个线程在负责事件的响应工作。

初始化线程

如代码所示,同时我们在初始化一个图形界面的时候,都会直接在主方法的主线程里,直接调用如下代码来进行初始化

new TestFrame().setVisible(true);


如果是小程序这没有什么问题,如果是复杂的程序就有可能产生问题了。因为这里有两个线程在同时访问组件:1. 主线程 2. 事件调度线程。 如果是复杂的图形界面程序,就有可能出现这两个线程同时操作的情况,导致同步问题的产生。

为了规避这个问题的产生,创建和显示界面的工作,最好也交给事件调度线程,这样就保证了只有一个线程在访问这些组件
 

SwingUtilities.invokeLater(new Runnable() {

public void run() {

new TestFrame().setVisible(true);

}

});


像这样,new TestFrame().setVisible(true); 这段代码就是在事件调度线程中执行了。

还可以使用SwingUtilities.isEventDispatchThread()来判断当前线程是否是事件调度线程

package gui;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;

public class TestGUI {
	public static void main(String[] args) {
		new TestFrame().setVisible(true);
		
//		SwingUtilities.invokeLater(new Runnable() {
//			public void run() {
//				new TestFrame().setVisible(true);
//			}
//		});
	}

	static class TestFrame extends JFrame {
		public TestFrame() {
			setTitle("LoL");

			setSize(400, 300);

			setLocation(200, 200);

			setLayout(null);

			JButton b = new JButton("一键秒对方基地挂");

			b.setBounds(50, 50, 280, 30);

			add(b);

			setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

			setVisible(true);
			
			System.out.println("当前线程是否是 事件调度线程: " + SwingUtilities.isEventDispatchThread());

		}
	}
}

事件调度线程

以 按钮监听 中的代码为例,ActionListener.actionPerformed 中的代码,就是事件调度线程执行的。

可以借助SwingUtilities.isEventDispatchThread() 确认,是事件调度线程在执行相应的代码

那么事件调度线程又是如何去执行这段代码的呢? 这就不用你操心啦, 要解释这个问题,就需要剖析整个Swing的框架,就不是本章节所能展示的内容啦

package gui;
  
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
  
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
  
public class TestGUI {
    public static void main(String[] args) {
  
        JFrame f = new JFrame("LoL");
        f.setSize(400, 300);
        f.setLocation(580, 200);
        f.setLayout(null);
  
        final JLabel l = new JLabel();
  
        ImageIcon i = new ImageIcon("e:/project/j2se/shana.png");
        l.setIcon(i);
        l.setBounds(50, 50, i.getIconWidth(), i.getIconHeight());
  
        JButton b = new JButton("隐藏图片");
        b.setBounds(150, 200, 100, 30);
  
        b.addActionListener(new ActionListener() {
  
            public void actionPerformed(ActionEvent e) {
                l.setVisible(false);
                
                System.out.println("当前使用的是事件调度线程:" + SwingUtilities.isEventDispatchThread());
            }
        });
  
        f.add(l);
        f.add(b);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  
        f.setVisible(true);
    }
}

长耗时任务线程

有时候需要执行长耗时任务,比如数据库查询,文件复制,访问网络等等。

而这些操作一般都会在事件响应后发起,就会自动进入事件调度线程。 而事件调度线程又是单线程模式,其结果就会是在执行这些长耗时任务的时候,界面就无响应了。

如图所示,当点击第一个按钮的时候,会在其中进行一个5秒钟的任务,这个期间,第一个按钮会保持按下状态,其他按钮也无法点击,出现了无响应了状态。

为了解决这个问题,Swing提供了一个SwingWorker类来解决。 SwingWorker是一个抽象类,为了使用,必须实现方法 doInBackground,在doInBackground中,就可以编写我们的任务,然后执行SwingWorker的execute方法,放在专门的工作线程中去运行。

SwingWorker worker = new SwingWorker() {

protected Object doInBackground() throws Exception {

//长耗时任务

return null;

}

};

worker.execute();


SwingWorker又是如何工作的呢?
当SwingWorker执行execute的时候,调用默认有10根线程的线程池,执行doInBackground中的代码,通过如下代码,可以获知执行当前SwingWorder的线程名称

System.out.println("执行这个SwingWorder的线程是:" + Thread.currentThread().getName());

package gui;

import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingWorker;

public class TestGUI {
	public static void main(String[] args) {

		JFrame f = new JFrame("LoL");
		f.setSize(300, 300);
		f.setLocation(200, 200);
		f.setLayout(new FlowLayout());

		JButton b1 = new JButton("在事件调度线程中执行长耗时任务");
		JButton b2 = new JButton("使用SwingWorker执行长耗时任务");
		JLabel l = new JLabel("任务执行结果");
		f.add(b1);
		f.add(b2);
		f.add(l);

		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		b1.addActionListener(new ActionListener() {

			@Override
			public void actionPerformed(ActionEvent e) {
				l.setText("开始执行完毕");
				try {
					Thread.sleep(5000);
				} catch (InterruptedException e1) {
					// TODO Auto-generated catch block
					e1.printStackTrace();
				}
				l.setText("任务执行完毕");
			}
		});
		b2.addActionListener(new ActionListener() {

			@Override
			public void actionPerformed(ActionEvent e) {
				SwingWorker<Void, Void> worker = new SwingWorker<Void, Void>() {

					@Override
					protected Void doInBackground() throws Exception {
						System.out.println("执行这个SwingWorder的线程是:" + Thread.currentThread().getName());
						l.setText("开始执行完毕");
						try {
							Thread.sleep(5000);
						} catch (InterruptedException e1) {
							// TODO Auto-generated catch block
							e1.printStackTrace();
						}
						l.setText("任务执行完毕");
						return null;
					}
				};
				worker.execute();

			}
		});

		f.setVisible(true);
	}
}

12、皮肤

Java提供了非常便捷的方式切换界面风格

设置皮肤

只需要提供一句代码

javax.swing.UIManager.setLookAndFeel("com.birosoft.liquid.LiquidLookAndFeel");


就可以把所有的组件切换成不同的风格。

注: 这句话需要加在最前面,如果已经创建了界面,再加这个有时候不能正常起作用。

在右侧的附件里提供了各种皮肤,以及皮肤对应的jar包的下载

package gui;
 
import javax.swing.JButton;
import javax.swing.JFrame;
 
public class TestGUI {
    public static void main(String[] args) {
        //设置皮肤 
        setLookAndFeel();

        JFrame f = new JFrame("LoL");
        f.setSize(400, 300);
        f.setLocation(200, 200);
        f.setLayout(null);
        JButton b = new JButton("一键秒对方基地挂");
        b.setBounds(50, 50, 280, 30);
 
        f.add(b);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
        f.setVisible(true);
    }
 
    private static void setLookAndFeel() {
        try {
          javax.swing.UIManager.setLookAndFeel("com.birosoft.liquid.LiquidLookAndFeel");
//          javax.swing.UIManager.setLookAndFeel("com.jtattoo.plaf.smart.SmartLookAndFeel");
//          javax.swing.UIManager.setLookAndFeel("com.jtattoo.plaf.mcwin.McWinLookAndFeel");
//          javax.swing.UIManager.setLookAndFeel("com.jtattoo.plaf.luna.LunaLookAndFeel");
//          javax.swing.UIManager.setLookAndFeel("com.jtattoo.plaf.aluminium.AluminiumLookAndFeel");
//          javax.swing.UIManager.setLookAndFeel("com.jtattoo.plaf.bernstein.BernsteinLookAndFeel");
//          javax.swing.UIManager.setLookAndFeel("com.jtattoo.plaf.hifi.HiFiLookAndFeel");
//          javax.swing.UIManager.setLookAndFeel("com.jtattoo.plaf.mint.MintLookAndFeel");
//          javax.swing.UIManager.setLookAndFeel("com.jtattoo.plaf.aero.AeroLookAndFeel");
//          javax.swing.UIManager.setLookAndFeel("com.jtattoo.plaf.fast.FastLookAndFeel");
//          javax.swing.UIManager.setLookAndFeel("com.jtattoo.plaf.acryl.AcrylLookAndFeel");
//          javax.swing.UIManager.setLookAndFeel("com.jtattoo.plaf.noire.NoireLookAndFeel"); 
        } catch (Exception e) {
            e.printStackTrace();
            // handle exception
        }
 
    }
}

1.9 网络编程

1、IP地址 端口

IP地址与端口概念

IP地址

在网络中每台计算机都必须有一个的IP地址;
32位,4个字节,常用点分十进制的格式表示,例如:192.168.1.100
127.0.0.1 是固定ip地址,代表当前计算机,相当于面向对象里的 "this"

端口

两台计算机进行连接,总有一台服务器,一台客户端。
服务器和客户端之间的通信通过端口进行。如图:

ip地址是 192.168.1.100的服务器通过端口 8080
与ip地址是192.168.1.189的客户端 的1087端口通信

获取本机IP地址

package socket;

import java.net.InetAddress;
import java.net.UnknownHostException;

public class TestSocket {

	public static void main(String[] args) throws UnknownHostException {
		InetAddress host = InetAddress.getLocalHost();
		String ip =host.getHostAddress();
		System.out.println("本机ip地址:" + ip);
	}
}

ping命令

使用ping判断一个地址是否能够到达
ping不是java的api,是windows中的一个小工具,用于判断一个地址的响应时间

如图
ping 192.168.2.106 可以返回这个地址的响应时间 time<1ms表示很快,局域网一般就是这个响应时间

ping 192.168.2.206 返回Request timed out表示时间较久都没有响应返回,基本判断这个地址不可用

使用Java执行ping命令

package socket;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class TestSocket {

	public static void main(String[] args) throws IOException {

		Process p = Runtime.getRuntime().exec("ping " + "192.168.2.106");
		BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
		String line = null;
		StringBuilder sb = new StringBuilder();
		while ((line = br.readLine()) != null) {
			if (line.length() != 0)
				sb.append(line + "\r\n");
		}
		System.out.println("本次指令返回的消息是:");
		System.out.println(sb.toString());
	}

}

2、Socket

使用 Socket(套接字)进行不同的程序之间的通信

建立连接

1. 服务端开启8888端口,并监听着,时刻等待着客户端的连接请求
2. 客户端知道服务端的ip地址和监听端口号,发出请求到服务端
客户端的端口地址是系统分配的,通常都会大于1024
一旦建立了连接,服务端会得到一个新的Socket对象,该对象负责与客户端进行通信。
注意: 在开发调试的过程中,如果修改过了服务器Server代码,要关闭启动的Server,否则新的Server不能启动,因为8888端口被占用了

package socket;
 
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
  
public class Server {
  
    public static void main(String[] args)  {
        try {
			//服务端打开端口8888
			ServerSocket ss = new ServerSocket(8888);
			  
			//在8888端口上监听,看是否有连接请求过来
			System.out.println("监听在端口号:8888");
			Socket s =  ss.accept();
			  
			System.out.println("有连接过来" + s);
			
			s.close();
			ss.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
          
    }
}

 收发数字

一旦建立了连接,服务端和客户端就可以通过Socket进行通信了
1. 客户端打开输出流,并发送数字 110
2. 服务端打开输入流,接受数字 110,并打印

package socket;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {

	public static void main(String[] args) {
		try {

			ServerSocket ss = new ServerSocket(8888);

			System.out.println("监听在端口号:8888");
			Socket s = ss.accept();

			//打开输入流
			InputStream is = s.getInputStream();

			//读取客户端发送的数据
			int msg = is.read();
			//打印出来
			System.out.println(msg);
			is.close();

			s.close();
			ss.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}
}

收发字符串

直接使用字节流收发字符串比较麻烦,使用数据流对字节流进行封装,这样收发字符串就容易了
1. 把输出流封装在DataOutputStream中
使用writeUTF发送字符串 "Legendary!"
2. 把输入流封装在DataInputStream
使用readUTF读取字符串,并打印

package socket;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {

	public static void main(String[] args) {
		try {

			ServerSocket ss = new ServerSocket(8888);

			System.out.println("监听在端口号:8888");
			Socket s = ss.accept();

			InputStream is = s.getInputStream();

			//把输入流封装在DataInputStream
			DataInputStream dis = new DataInputStream(is);
			//使用readUTF读取字符串
			String msg = dis.readUTF();
			System.out.println(msg);
			dis.close();
			s.close();
			ss.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}
}

使用Scanner

在上个步骤中,每次要发不同的数据都需要修改代码
可以使用Scanner读取控制台的输入,并发送到服务端,这样每次都可以发送不同的数据了。

package socket;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;

public class Client {

	public static void main(String[] args) {

		try {
			Socket s = new Socket("127.0.0.1", 8888);

			OutputStream os = s.getOutputStream();
	        DataOutputStream dos = new DataOutputStream(os);
	        
	        //使用Scanner读取控制台的输入,并发送到服务端
	        Scanner sc = new Scanner(System.in);
	        
	        String str = sc.next();
	        dos.writeUTF(str);
	        
			dos.close();
			s.close();
		} catch (UnknownHostException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

3、多线程聊天

如果使用单线程开发Socket应用,那么同一时间,要么收消息,要么发消息,不能同时进行。

为了实现同时收发消息,就需要用到多线程

同时收发消息

练习-服务端和客户端互聊 中,只能一人说一句,说了之后,必须等待另一个人的回复,才能说下一句。

这是因为接受和发送都在主线程中,不能同时进行。 为了实现同时收发消息,基本设计思路是把收发分别放在不同的线程中进行

1. SendThread 发送消息线程
2. RecieveThread 接受消息线程
3. Server一旦接受到连接,就启动收发两个线程
4. Client 一旦建立了连接,就启动收发两个线程

package socket;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

public class SendThread extends Thread{

	private Socket s;

	public SendThread(Socket s){
		this.s = s;
	}
	public void run(){
		try {
			OutputStream os = s.getOutputStream();
			DataOutputStream dos = new DataOutputStream(os);

			while(true){
			    Scanner sc = new Scanner(System.in);
			    String str = sc.next();
			    dos.writeUTF(str);
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
	}
	
}

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_52550583/article/details/123834762

智能推荐

稀疏编码的数学基础与理论分析-程序员宅基地

文章浏览阅读290次,点赞8次,收藏10次。1.背景介绍稀疏编码是一种用于处理稀疏数据的编码技术,其主要应用于信息传输、存储和处理等领域。稀疏数据是指数据中大部分元素为零或近似于零的数据,例如文本、图像、音频、视频等。稀疏编码的核心思想是将稀疏数据表示为非零元素和它们对应的位置信息,从而减少存储空间和计算复杂度。稀疏编码的研究起源于1990年代,随着大数据时代的到来,稀疏编码技术的应用范围和影响力不断扩大。目前,稀疏编码已经成为计算...

EasyGBS国标流媒体服务器GB28181国标方案安装使用文档-程序员宅基地

文章浏览阅读217次。EasyGBS - GB28181 国标方案安装使用文档下载安装包下载,正式使用需商业授权, 功能一致在线演示在线API架构图EasySIPCMSSIP 中心信令服务, 单节点, 自带一个 Redis Server, 随 EasySIPCMS 自启动, 不需要手动运行EasySIPSMSSIP 流媒体服务, 根..._easygbs-windows-2.6.0-23042316使用文档

【Web】记录巅峰极客2023 BabyURL题目复现——Jackson原生链_原生jackson 反序列化链子-程序员宅基地

文章浏览阅读1.2k次,点赞27次,收藏7次。2023巅峰极客 BabyURL之前AliyunCTF Bypassit I这题考查了这样一条链子:其实就是Jackson的原生反序列化利用今天复现的这题也是大同小异,一起来整一下。_原生jackson 反序列化链子

一文搞懂SpringCloud,详解干货,做好笔记_spring cloud-程序员宅基地

文章浏览阅读734次,点赞9次,收藏7次。微服务架构简单的说就是将单体应用进一步拆分,拆分成更小的服务,每个服务都是一个可以独立运行的项目。这么多小服务,如何管理他们?(服务治理 注册中心[服务注册 发现 剔除])这么多小服务,他们之间如何通讯?这么多小服务,客户端怎么访问他们?(网关)这么多小服务,一旦出现问题了,应该如何自处理?(容错)这么多小服务,一旦出现问题了,应该如何排错?(链路追踪)对于上面的问题,是任何一个微服务设计者都不能绕过去的,因此大部分的微服务产品都针对每一个问题提供了相应的组件来解决它们。_spring cloud

Js实现图片点击切换与轮播-程序员宅基地

文章浏览阅读5.9k次,点赞6次,收藏20次。Js实现图片点击切换与轮播图片点击切换<!DOCTYPE html><html> <head> <meta charset="UTF-8"> <title></title> <script type="text/ja..._点击图片进行轮播图切换

tensorflow-gpu版本安装教程(过程详细)_tensorflow gpu版本安装-程序员宅基地

文章浏览阅读10w+次,点赞245次,收藏1.5k次。在开始安装前,如果你的电脑装过tensorflow,请先把他们卸载干净,包括依赖的包(tensorflow-estimator、tensorboard、tensorflow、keras-applications、keras-preprocessing),不然后续安装了tensorflow-gpu可能会出现找不到cuda的问题。cuda、cudnn。..._tensorflow gpu版本安装

随便推点

物联网时代 权限滥用漏洞的攻击及防御-程序员宅基地

文章浏览阅读243次。0x00 简介权限滥用漏洞一般归类于逻辑问题,是指服务端功能开放过多或权限限制不严格,导致攻击者可以通过直接或间接调用的方式达到攻击效果。随着物联网时代的到来,这种漏洞已经屡见不鲜,各种漏洞组合利用也是千奇百怪、五花八门,这里总结漏洞是为了更好地应对和预防,如有不妥之处还请业内人士多多指教。0x01 背景2014年4月,在比特币飞涨的时代某网站曾经..._使用物联网漏洞的使用者

Visual Odometry and Depth Calculation--Epipolar Geometry--Direct Method--PnP_normalized plane coordinates-程序员宅基地

文章浏览阅读786次。A. Epipolar geometry and triangulationThe epipolar geometry mainly adopts the feature point method, such as SIFT, SURF and ORB, etc. to obtain the feature points corresponding to two frames of images. As shown in Figure 1, let the first image be ​ and th_normalized plane coordinates

开放信息抽取(OIE)系统(三)-- 第二代开放信息抽取系统(人工规则, rule-based, 先抽取关系)_语义角色增强的关系抽取-程序员宅基地

文章浏览阅读708次,点赞2次,收藏3次。开放信息抽取(OIE)系统(三)-- 第二代开放信息抽取系统(人工规则, rule-based, 先关系再实体)一.第二代开放信息抽取系统背景​ 第一代开放信息抽取系统(Open Information Extraction, OIE, learning-based, 自学习, 先抽取实体)通常抽取大量冗余信息,为了消除这些冗余信息,诞生了第二代开放信息抽取系统。二.第二代开放信息抽取系统历史第二代开放信息抽取系统着眼于解决第一代系统的三大问题: 大量非信息性提取(即省略关键信息的提取)、_语义角色增强的关系抽取

10个顶尖响应式HTML5网页_html欢迎页面-程序员宅基地

文章浏览阅读1.1w次,点赞6次,收藏51次。快速完成网页设计,10个顶尖响应式HTML5网页模板助你一臂之力为了寻找一个优质的网页模板,网页设计师和开发者往往可能会花上大半天的时间。不过幸运的是,现在的网页设计师和开发人员已经开始共享HTML5,Bootstrap和CSS3中的免费网页模板资源。鉴于网站模板的灵活性和强大的功能,现在广大设计师和开发者对html5网站的实际需求日益增长。为了造福大众,Mockplus的小伙伴整理了2018年最..._html欢迎页面

计算机二级 考试科目,2018全国计算机等级考试调整,一、二级都增加了考试科目...-程序员宅基地

文章浏览阅读282次。原标题:2018全国计算机等级考试调整,一、二级都增加了考试科目全国计算机等级考试将于9月15-17日举行。在备考的最后冲刺阶段,小编为大家整理了今年新公布的全国计算机等级考试调整方案,希望对备考的小伙伴有所帮助,快随小编往下看吧!从2018年3月开始,全国计算机等级考试实施2018版考试大纲,并按新体系开考各个考试级别。具体调整内容如下:一、考试级别及科目1.一级新增“网络安全素质教育”科目(代..._计算机二级增报科目什么意思

conan简单使用_apt install conan-程序员宅基地

文章浏览阅读240次。conan简单使用。_apt install conan