功能流、图形化界面、多线程
-
功能流介绍
-
序列化和反序列化流
-
序列化和反序列化介绍
-
-
我们使用new关键字创建出来的对象,都保存在内存的堆中。而类中的所有非静态的成员随着对象的创建会在堆中出现。而我们在前面学习的任何的IO流对象,它们都是将某个变量、数组、或者集合中的数据保存到文件中。现在我们要学习的序列化和反序列化流它们的主要功能是将堆中的这个对象给我们长久的保存在文件中。
序列化:将堆中的对象(包含对象中的所有数据)给些文件中。
反序列化:将文件中保存的持久的对象从新读取到程序中。
-
序列化流
ObjectOutputStream 将 Java 对象的基本数据类型和图形写入 OutputStream。可以使用 ObjectInputStream 读取(重构)对象。通过在流中使用文件可以实现对象的持久存储。如果流是网络套接字流,则可以在另一台主机上或另一个进程中重构对象。
ObjectOutputStream:它的功能是将堆中的对象中的所有数据,包含当前这个对象所属类的一些信息给些到文件中。其实ObjectOutputStream它本质不能和文件交互,而它的主要功能是将对象以及类中的数据进行编码的。在编码之后使用创建对象时传递的OutputStream的对象将字节写到文件中。
/*
* 演示对象的序列化
*/
public class ObjectOutputStreamDemo {
public static void main(String[] args) throws IOException {
// 创建需要被持久保存的Person对象
Person p = new Person("班长",23);
// 创建用于序列化对象的那个序列化流对象
ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream( "e:/Object.obj" ) );
// 写对象
oos.writeObject( p );
// 关流
oos.close();
}
}
上面的程序在运行的时候发生异常:
异常的原因:是因为在Java中规定,如果一个类的对象需要被序列化,这个对象所属的类必须符合Java中对象序列化的标准。
对象能够被序列化的标准:
要求在java中,可以被序列化的对象所属类必须实现Java中定义的序列化接口。
只有实现了序列化接口的类的对象,才能被ObjectOutputStream这个流将对象长久的保存在文件中。
Serializable :接口是Java中定义出来的专门用于标记某个类的对象能否被JVM持久保存的接口。因此在Java中,如果一个接口中没有任何方法,这个接口也被称为标记型接口。
上面的异常解决:让Person实现序列化接口。
-
反序列化
ObjectInputStream:它本身也不能从底层读取数据。需要在创建ObjectInputStream对象的时候给其传递一个可以从底层读取字节数据的字节输入流对象,底层字节输入流将字节读取到程序中,ObjectInputStream它内部会根据的当前序列化时的一些信息,将读取到的对象给我们重新放到堆中,进而我们就可以直接去使用这个对象,而不需要重新new对象。
/*
* 演示反序列化
*/
public class ObjectInputStreamDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 创建反序列化对象
ObjectInputStream ois = new ObjectInputStream( new FileInputStream("e:/Object.obj") );
// 读取对象
Object obj = ois.readObject();
System.out.println(obj);
// 关流
ois.close();
}
}
反序列化的时候,有时会发生异常:
Exception in thread "main" java.io.InvalidClassException: cn.itcast.sh.a_obj.Person; local class incompatible: stream classdesc serialVersionUID = 8955961059194758694, local class serialVersionUID = -783203336827082711
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:617)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1622)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1517)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1771)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1350)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:370)
at cn.itcast.sh.a_obj.ObjectInputStreamDemo.main(ObjectInputStreamDemo.java:18)
上面的异常原因:
是我们在序列化的时候,使用的class文件和反序列化回来之后进行对比的class文件不是同一个。
解决方案:需要在被序列化的类中人为添加序列化版本号:
序列化和反序列化需要注意的两个问题:
-
被序列化的对象所属的类需要实现序列化接口。
-
被序列化的类需要添加一个序列化版本号。
-
序列化细节
-
序列化只能将对象在堆中的数据随着对象一起写到文件中,如果类中有静态的成员变量是不会被写出的。
-
被瞬态关键字修饰的非静态成员变量也不会被序列化
-
-
打印流
-
打印流介绍
-
打印流属于输出流,主要是将数据打印到不同的目的地。
打印流可以将数据写到:文件、控制台、网络、其他的打印设备等目的地。
打印流:
PrintStream:字节打印流
PrintWriter:字符打印流
-
PrintStream介绍
/*
* 演示PrintStream流对象
*/
public class PrintStreamDemo {
public static void main(String[] args) {
System.out.println("Hello World!!!");
PrintStream ps = System.out;
ps.println("aaaa");
}
}
-
PrintWriter介绍:
/*
* 演示 PrintWriter
*/
public class PrintWriterDemo {
public static void main(String[] args) throws IOException {
method2();
}
/*
* 演示PrintWriter的自动刷新功能
*
* 如果我们使用PrintWriter输出数据,启动自动刷新功能,仅仅只有在调用
* println \ printf \ format 方法会刷新
*/
public static void method2() throws IOException {
// 创建对象 对象第一个参数传递的流对象,第二次参数绝对是否会在输出的时候自动刷新
PrintWriter pw = new PrintWriter( new FileWriter("e:/pw.txt") , true);
//pw.println("aaaa");
pw.write("aaaa");
}
public static void method1() throws FileNotFoundException, UnsupportedEncodingException {
// 创建对象
PrintWriter pw = new PrintWriter("e:/pw.txt" , "utf-8");
// 写数据
pw.println("aaaa");
pw.close();
}
}
-
图形化界面
-
图形界面介绍
-
在第一天学习中,介绍软件的运行有两种方式:
-
图形化界面
-
命令行
GUI
Graphical User Interface(图形用户接口)。
用图形的方式,来显示计算机操作的界面,这样更方便更直观。
CLI
Command line User Interface (命令行用户接口)
就是常见的Dos命令行操作。
需要记忆一些常用的命令,操作不直观。
Java中的图形化界面:
早期sun公司研发一套用于开发图形化界面的程序,全部保存在java.awt包下。
awt包下的所有类和接口,它们本身不能创建界面,而是sun公司根据操作系统的特点,将windows和linux系统的共同的界面特征进行总结,最后操作系统自身的功能使用Java进行了封装。
awt包下的所有创建界面的类和接口,严重依赖操作系统。
后期sun公司对界面进行升级:javax.swing包下。这个包下的类和接口创建的界面是java语言自己的独立界面不依赖操作系统。
后期如果需要开发Java的图形化界面:一般都使用第三方公司开发的图形界面。例如:IBM公司的SWT。
-
图形化界面类关系
在Java的图形化界面中,将每个对象都称为组件。需要创建出界面之后,将对应的组件添加到界面上。
-
布局管理器
布局:设计页面上每个组件摆放位置。
-
简单窗体实现
/*
* 演示Java的窗口
*/
public class MyWindow {
public static void main(String[] args) {
// 创建一个窗口
//Window w = new Window( new Frame() );
Frame w = new Frame("我的丑窗体");
// 设置大小
w.setSize(300, 200);
// 设置布局
w.setLayout( new FlowLayout());
// 创建按钮
Button btn = new Button("确定");
// 将按钮添加到界面上
w.add(btn);
// 让窗口可见
w.setVisible(true);
}
}
-
事件监听
事件监听:我们可以通过键盘、鼠标等设备对软件中的某些组件进行操作。而在这些组件上就会发生鼠标、键盘等操作的结果,在操作的过程中,需要对鼠标、键盘给出的操作作出对应的反应。操作过程中的具体的动作被称为事件。
事件源:
发生事件的源头对象。
事件:
在事件源上发生的动作。
监听器:
负责监听事件源上发生的动作(事件)的对象。
监听器的作用:它的主要作用是监听事件源上发生的事件,需要针对不同的事件给出不同的处理方案。在Java的图形化界面上:
界面、组件它们属于事件源,我们的鼠标,键盘,等都输外力,我们需要给界面、组件上面注册监听器,在监听器中需要根据不同的事件处理不同的结果。
-
监听演示
-
窗口监听
-
-
动作监听
/*
* 演示动作监听
*/
public class WindowDemo {
public static void main(String[] args) {
JFrame.setDefaultLookAndFeelDecorated(true);
JFrame jf = new JFrame("动作监听演示");
// 设置布局
jf.setLayout( new FlowLayout() );
// 下面代码相当于给窗口注册了关闭的监听
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 静态方法设置窗口样式
// 设置大小
jf.setSize(300, 200);
// 动作监听 , 点击按钮,将窗口关闭
JButton btn = new JButton("关闭");
// 给按钮上添加动作监听
btn.addActionListener( new ActionListener(){
public void actionPerformed(ActionEvent e) {
System.exit(0);
}
});
// 将按钮添加到窗口上
jf.add(btn);
// 设置窗口可见
jf.setVisible(true);
}
}
-
鼠标监听
/*
* 演示鼠标监听
*/
public class WindowDemo2 {
public static void main(String[] args) {
JFrame.setDefaultLookAndFeelDecorated(true);
init();
}
public static void init() {
final JFrame jf = new JFrame("动作监听演示");
// 设置布局
jf.setLayout( new FlowLayout() );
// 下面代码相当于给窗口注册了关闭的监听
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 静态方法设置窗口样式
// 设置大小
//jf.setSize(300, 200);
Random r = new Random();
jf.setBounds(r.nextInt(1000), r.nextInt(600), 300, 200);
// 点击按钮,将窗口关闭,在其他位置继续打开
JButton btn = new JButton("来抓我!!!你抓不到");
btn.addMouseListener( new MouseAdapter(){
@Override
public void mouseEntered(MouseEvent e) {
// 隐藏窗口,窗口在其他位置打开
jf.dispose();
init();
}
});
// 将按钮添加到窗口上
jf.add(btn);
// 设置窗口可见
jf.setVisible(true);
}
}
-
键盘监听
/*
* 演示键盘监听
*/
public class WindowDemo3 {
public static void main(String[] args) {
JFrame.setDefaultLookAndFeelDecorated(true);
init();
}
public static void init() {
final JFrame jf = new JFrame("动作监听演示");
// 设置布局
jf.setLayout( new FlowLayout() );
// 下面代码相当于给窗口注册了关闭的监听
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 静态方法设置窗口样式
// 设置大小
//jf.setSize(300, 200);
Random r = new Random();
jf.setBounds(r.nextInt(1000), r.nextInt(600), 300, 200);
// 创建文本框
JTextField tf = new JTextField(10);
// 给文本框上添加键盘监听
tf.addKeyListener( new KeyListener() {
// 键入 , 输入某个字符
@Override
public void keyTyped(KeyEvent e) {
int code = e.getKeyCode();
char char1 = e.getKeyChar();
String text = e.getKeyText(code);
System.out.println(code +"...."+char1 + "....."+ text);
// 阻止某些字符输入到文本框中
if( !(char1 >= '0' && char1 <= '9') ){
e.consume(); // 阻止
}
}
// 释放,松开按键的瞬间
@Override
public void keyReleased(KeyEvent e) {
}
// 按下 按键的瞬间
@Override
public void keyPressed(KeyEvent e) {
System.out.println("按下");
}
});
// 将按钮添加到窗口上
jf.add(tf);
// 设置窗口可见
jf.setVisible(true);
}
}
-
验证码实现
验证码:它的实现主要是在一个图片上写几个简单字符,让用户输入。
/*
* 验证码
*/
public class CheckImg {
public static void main(String[] args) throws IOException {
int width = 90;
int height = 28;
// 准备内存中的画纸(图片的缓冲区)
BufferedImage bufi = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
// 获取画笔,给图片上画内容
Graphics g = bufi.getGraphics();
// 设置画笔的颜色
g.setColor( Color.WHITE );
// 画背景
g.fillRect(0, 0, width, height);
// 设置画笔颜色
g.setColor( Color.RED );
// 画边框
g.drawRect(0, 0, width-1, height-1);
// 准备需要给图片上写数据
String data = "1234567890QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm";
// 需要随机数
Random r = new Random();
int x = 8;
// 使用循环给图片上写字符
for( int i = 0 ; i < 4 ; i++ ){
// 设置字体
g.setFont( new Font("宋体" , Font.BOLD , 23));
// 设置颜色
g.setColor( new Color( r.nextInt(100) , r.nextInt(100) ,r.nextInt(100)) );
// 获取写的数据
char ch = data.charAt(r.nextInt( data.length() ));
// 写数据
g.drawString(ch + "", x, 23);
x+=18;
}
// 添加干扰信息
for( int i = 0 ; i < 18 ; i++ ){
// 设置颜色
g.setColor( new Color( r.nextInt(150) +100 , r.nextInt(150)+100 , r.nextInt(150)+100 ) );
// 画线
g.drawLine( r.nextInt( width ), r.nextInt( height ), r.nextInt( width ), r.nextInt( height ));
}
// 输出
ImageIO.write(bufi, "JPG", new FileOutputStream("e:/check.jpg"));
}
}
-
多线程技术介绍
-
进程介绍
-
我们给电脑上安装的软件都在硬盘上保存。当我们找到软件的入口程序之后,双击运行。程序在运行的时候是将硬盘上的程序加载到内存中运行。软件在运行的过程中,它首先会在内存中划分出属于自己运行的空间。
负责运行软件的那个内存中间,我们成为当前这个软件在内存中的一个进程。
每个软件的运行都需要一个独立的进程来负责。进程彼此之间不会影响。
任何的软件运行在内存中至少有一个进程。
软件运行过程中:会有一个负责软件的主程序(主进程),负责其他功能的(子进程)。
-
线程介绍
进程是负责整个软件的运行。但是软件中会有独立的功能需要独立运行。
这时这些负责运行独立功能的内存空间,它们必须位于软件的进程中。这些内存空间我们称之为线程。
线程它才是真正负责运行软件的具体的功能的内存空间。
进程中最少会有一个线程。
-
多线程介绍
一个软件运行的过程中,内部都多个功能需要运行,这时会在软件运行的进程中划分出多个子的空间,每个空间都属于一个线程。这种情况我们就称为多线程。
目前几乎所有的软件都支持多线程。软件运行之后,在我们的内存中同时(并发)会有多个线程在运行。
同时(并发):在某个时间点上,有多个线程在运行。
-
多线程运行原理
多线程运行:计算机中的任何的运算和运行最终都由CPU处理。其实我们CPU它某个时间点上只能运行一个线程。但是由于CPU它的切换速度太快,导致我们肉眼根本无法分别。因此我们错误的感觉到多个软件可以同时运行。
上面的解释它主要是针对单核CPU。如果是多核心的CPU,那么每个时间点是可以真正同时处理多个线程。
-
Java中线程描述
-
主线程介绍
-
在命令行中输入java 类名 回车之后,JVM开始运行,其实就是JVM在内存中划分Java运行需要的进程。然后在这个进程中开启一个线程,这个线程来负责运行当前类中的main方法。进程中的负责运行我们main方法的那个线程就称为主线程。
上面的程序运行的过程中,不管是main方法中的代码还是Demo类中的show方法它们都是主线程在运行的。当在main方法中执行d.show()代码的时候,主线程就会将show方法加载到栈内存中运行,导致main方法最在的栈区会被下压。当把show方法执行完成,show方法出栈,主线程才会继续执行main方法中其他的代码。整个程序只有主线程在运行。
-
Thread类介绍
线程是软件在运行的过程中底层存在的一类事物,那么Java就使用Thread类来描述和封装线程这类事物。只要我们想在Java中操作线程,这时只需要通过Thread就可以完成。
Thread 是程序中的执行线程。Java 虚拟机允许应用程序并发(同时)地运行多个执行线程。
Thread类的api告诉我们:创建新的线程有两种方式:
1、定义类继承Thread
2、定义类实现Runnable接口
-
创建线程
-
第一种方式
-
继承Thread类创建线程(重点)
-
-
一种方法是将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。接下来可以分配并启动该子类的实例。
创建新线程步骤:
1、定一个类,继承Thread
2、子类中需要重写父类的run方法
3、创建子类的对象
4、开启新的线程,让新线程运行
-
代码实现(编码练习)
// 定义类继承Thread
class Demo2 extends Thread{
// 重写run方法
public void run() {
for( int i = 0 ; i < 20 ; i++){
System.out.println("Demo2 run i = " + i );
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
// 创建子类对象
Demo2 d = new Demo2();
// 启动线程
d.start();
for( int i = 0 ; i < 20 ; i++){
System.out.println("main = " + i );
}
}
}
-
多线程运行内存图解(理解)
图解说明:
程序在运行的时候,只有主线程,在main方法中执行到t.start() 代码之后,这时JVM就会开启一个新的线程,那么程序中就会有主线程和新的线程两个线程同时在运行,cpu切换到那个线程上,这个线程对应的代码就会执行。
-
总结第一种方式
-
为什么要继承Thread类(理解)
-
Thread类本身是描述线程的。也就是说我们可以直接通过Thread类创建线程,并开启线程。我们在程序开启新的线程的目的是为了让程序中有个线程可以同时去执行不同的代码,进而提高程序的运行效率。
Thraed类它本身是Java中提供的描述线程的类,如果我们直接创建Thread类的对象,而这个Thread类不是我们自己定义的类,sun公司在早期研发JDK的时候就已经将Thread类中的所有代码书写完成,而现在如果我们直接创建Thread类,可以得到线程,但是没有办法让线程运行后期我们自己某些程序代码。
我们让自己的类继承Thread类,主要是希望自己类中的某个方法,可以被线程运行。同时如果我们的类继承Thread类,那么我们的类就变成线程类。相当于我们的类可以直接去操作线程。
-
为什么要复写run方法(理解)
只要创建了线程对象,线程被启动之后,JVM会自动的调用线程类中的run方法。也就是说只要写在run方法中的代码就会被线程执行。而我们创建线程的目的就是让线程执行我们自己书写的代码,也就是我们只要想办法把代码书写在run方法中,肯定可以被线程执行。
如果我们不复写run方法,线程被开启执行,这时就会去调用Thread类中的run方法,而Thread类中的run方法是sun公司早就写好的run方法,Thread类中的run方法中肯定不会有我们自己的代码。也就是说线程在运行的时候,我们只要复写调用Thread类中的run方法,那么在创建子类对象的时候,按照我们学习的多态的技术,最终方法运行的是子类自己的。
复写run方法的目的:就是给线程明确线程要执行的代码。线程要执行的代码,被称为线程的任务。
-
为什么不直接调用run方法,而调用start方法(理解)
创建了线程对象(new Thread 或 Thread的子类),这时仅仅只有线程,而线程并不会运行。如果需要线程运行,必须调用Thread类中的start方法。
如果我们在程序中直接创建Thread或Thread的子类对象,然后通过对象调用run方法,那么虽然有线程,但是线程没有开启。这时的run方法运行和线程本身没有关系。
因此我们需要通过start方法将线程开启,开启之后,JVM会自动的将run方法加载新线程的栈区运行。这样就会导致程序中有多个线程被cpu同时运行。
-
创建线程第二种方式
-
第二种方式步骤(记忆)
-
另一种方法是声明实现 Runnable 接口的类。该类然后实现 run 方法。然后可以分配该类的实例,在创建 Thread 时作为一个参数来传递并启动。
步骤:
1、定义类实现Runnable接口
2、实现类中实现run方法
3、创建实现类的对象
4、创建Thread类的对象,然后将实现类的对象作为参数传递给Thread的构造方法
5、开启新的线程
-
代码实现(编码练习)
/*
* 演示创建Thread的第二种方式
*/
// 定义类实现Runnable接口
class Demo3 implements Runnable{
//实现run方法
public void run(){
for( int i = 0 ; i < 20 ; i++ ){
System.out.println("Demo3 run i = " + i );
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
// 创建实现类的对象
Demo3 d = new Demo3();
// 创建Thread类的对象,将实现类对象作为参数传递
Thread t = new Thread( d );
// 开启新线程
t.start();
for( int i = 0 ; i < 20 ; i++ ){
System.out.println("main i = " + i );
}
}
}
-
总结实现Runnable接口原理
-
继承的弊端(理解)
-
在Java中规定一个类只能继承一个父类(单继承),在真正开发中,如果有类中部分代码需要被线程执行,这时我们如果采用第一种方案,就会使当前的这个类脱离原来的继承关系,而去继承Thread类。那么这种操作非常不友好。
例如:Dog类中有个代码需要被多线程执行,这时假设Dog类它的父类是Animal,而如果使用第一种方式,就会导致Dog类脱离Animal类。而是Dog变成线程类。这种使用多线程的方案肯定不可行。
sun公司给出的第二种方案:
如果某个类中有代码需要被线程执行,这个类不用改变原有的继承关系,而只需要这个类实现接口。那么类中的代码书写在run方法中,就可以被线程运行。
-
接口存在的意义(理解)
接口存在的意义:
1、给事物体系增加扩展功能
2、给事物双方定义规则
我们Runnable接口它在这里充当的规则。
创建线程的第二种方式正好符合和软件行业中"高内聚,低耦合"。
高内聚:软件内部各个不需要外界的模块之间紧密联系程度达到最高要求。
低耦合:如果不相关的功能之间,最好没有任何的联系。
Thread类它是负责线程,描述线程,也就是Thread类只要去负责怎么开启线程,关闭线程,优化线程,线程之间的停止和调用等。
线程要执行的代码(任务),而这个必须放在run方法中,放在run方法中的代码需要后期开发中才能指定。相当于我们需要将线程自身的一些功能和线程后期要执行的任务进行分离。
Thread类只负责和线程自身相关的功能,让Runnable接口负责线程后期要执行的任务。
-
多线程练习
-
获取线程名称
-
每个线程都有一个标识名,多个线程可以同名。如果线程创建时没有指定标识名,就会为其生成一个新名称。
任何一个线程都有一个名称。
使用Thread类中的构造方法可以在创建线程的时候给线程命名:
Thread构造方法中接收的String name 就是为当前这个线程对象指定的名称。一般我们在创建线程的时候很少给线程命名。
如果在创建线程的时候没有给线程命名,这时创建好的线程会有默认的名称:Thread-x x从0开始。
修改线程的名称:
获取线程的名称:
在其他没有Thread对象的地方,获取线程的名称:
使用Thread类中的静态方法,获取当前的线程对象:
-
售票案例(编码练习)
-
分析
-
车站窗口售票:
我们可以在任何一个窗口买到当前某趟列车上某行车票,车站的所有窗口是同时(并发)的售票,但是如果我们在任何一个窗口将当前这趟列车上一张票买走,其他任何窗口都不能在出售这张票。
上面的解释:窗口在同时(并发)售票,这时每个窗口我们可以看成一个线程,所有线程在操作同一个趟列车上的票。只要出售,总票数就会减少。如果票出售到零,说明当前这趟列车上就已经不能在继续出售票。
-
代码实现
/*
* 售票案例简单实现
*/
class Ticket implements Runnable{
//定义成员变量,用来充当总票数
private int num = 100;
public void run(){
// 因为窗口售票是重复进行,只要有票窗口就不会停止
while( true){
if( num > 0 ){
// 使用打印语句代表出售了一张票
System.out.println( Thread.currentThread().getName() + " 正在售出的票是 :" + num );
num--;
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
// 创建线程的任务对象
Ticket task= new Ticket();
// 创建线程对象
Thread t = new Thread( task );
Thread t2 = new Thread( task );
Thread t3 = new Thread( task );
Thread t4 = new Thread( task );
// 开启线程
t.start();
t2.start();
t3.start();
t4.start();
}
}
-
线程安全
-
安全问题分析(重点)
-
-
多线程安全问题解决(重点+编码)
-
同步细节
-
多线程中同步细节
-
-
JDK中和同步相关类
-
同步利弊