首页编程java编程java多线程详解(java多线程的定义)

java多线程详解(java多线程的定义)

编程之家2026-06-04929次浏览

大家好,关于java多线程详解很多朋友都还不太明白,今天小编就来为大家分享关于java多线程的定义的知识,希望对各位有所帮助!

java多线程详解(java多线程的定义)

什么是java多线程详解

线程对象是可以产生线程的对象。比如在Java平台中Thread对象,Runnable对象。线程,是指正在执行的一个指点令序列。在java平台上是指从一个线程对象的start()开始,运行run方法体中的那一段相对独立的过程。相比于多进程,多线程的优势有:

(1)进程之间不能共享数据,线程可以;

(2)系统创建进程需要为该进程重新分配系统资源,故创建线程代价比较小;

(3)Java语言内置了多线程功能支持,简化了java多线程编程。

一、创建线程和启动

(1)继承Thread类创建线程类

java多线程详解(java多线程的定义)

通过继承Thread类创建线程类的具体步骤和具体代码如下:

•定义一个继承Thread类的子类,并重写该类的run()方法;

•创建Thread子类的实例,即创建了线程对象;

•调用该线程对象的start()方法启动线程。

复制代码

class SomeThead extends Thraad{

java多线程详解(java多线程的定义)

public void run(){

//do something here

}

}

public static void main(String[] args){

SomeThread oneThread= new SomeThread();

步骤3:启动线程:

oneThread.start();

}

复制代码

(2)实现Runnable接口创建线程类

通过实现Runnable接口创建线程类的具体步骤和具体代码如下:

•定义Runnable接口的实现类,并重写该接口的run()方法;

•创建Runnable实现类的实例,并以此实例作为Thread的target对象,即该Thread对象才是真正的线程对象。

复制代码

class SomeRunnable implements Runnable{

public void run(){

//do something here

}

}

Runnable oneRunnable= new SomeRunnable();

Thread oneThread= new Thread(oneRunnable);

oneThread.start();

复制代码

(3)通过Callable和Future创建线程

通过Callable和Future创建线程的具体步骤和具体代码如下:

•创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。

•创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

•使用FutureTask对象作为Thread对象的target创建并启动新线程。

•调用FutureTask对象的get()方法来获得子线程执行结束后的返回值其中,Callable接口(也只有一个方法)定义如下:

复制代码

public interface Callable{

V call() throws Exception;

}

步骤1:创建实现Callable接口的类SomeCallable(略);

步骤2:创建一个类对象:

Callable oneCallable= new SomeCallable();

步骤3:由Callable创建一个FutureTask对象:

FutureTask oneTask= new FutureTask(oneCallable);

注释: FutureTask是一个包装器,它通过接受Callable来创建,它同时实现了 Future和Runnable接口。

步骤4:由FutureTask创建一个Thread对象:

Thread oneThread= new Thread(oneTask);

步骤5:启动线程:

oneThread.start();

Java多线程之ThreadPoolExecutor原理(图文代码实例详解)

ThreadPoolExecutor是Java的线程池并发代名词,多线程开发基本都是基于这个去做具体的业务开发。虽然觉得自己回了,网上帖子已经有很多的文章写这个,但是是自己一一点写的,终归是要比看别人的理解更加深刻,所以最近自己在对java知识的系统梳理。那么接下来主要分析下这个多线程框架的原理。

ThreadPoolExecutor的构造函数以成员变量介绍publicThreadPoolExecutor(intcorePoolSize,intmaximumPoolSize,longkeepAliveTime,TimeUnitunit,BlockingQueue<Runnable>workQueue,ThreadFactorythreadFactory,RejectedExecutionHandlerhandler){面试靠的最多是这个构造函数中7个参数的作用,

corePoolSize是核心线程数,即使线程是空闲的,线程池一直保持的的线程数,除非allowCoreThreadTimeOut参数设置为true

maximumPoolSize线程池最大线程数

keepAliveTimeunit线程存活时间和时间单位

workQueue是任务队列,是用来保持task,该队列保持住了Runnable的任务,通过调用线程池的execute的方法.

threadFactory创建线程的工厂

RejectedExecutionHandler是当线程数超过限制以及队列也满了,需要执行的拒绝策略.

成员变零

privatefinalAtomicIntegerctl=newAtomicInteger(ctlOf(RUNNING,0));privatestaticfinalintCOUNT_BITS=Integer.SIZE-3;privatestaticfinalintCAPACITY=(1<<COUNT_BITS)-1;//线程容量//runStateisstoredinthehigh-orderbitsprivatestaticfinalintRUNNING=-1<<COUNT_BITS;privatestaticfinalintSHUTDOWN=0<<COUNT_BITS;privatestaticfinalintSTOP=1<<COUNT_BITS;privatestaticfinalintTIDYING=2<<COUNT_BITS;privatestaticfinalintTERMINATED=3<<COUNT_BITS;面试最喜欢问的是ctl变量的代表什么意义?ctl变量的的的用高3位表示线程池的状态,用低29位表示线程个数,两者通过|操作,拼接出ctl变量,也就是线程池的最大线程数capacity是(2^29)-1。

线程池状态RUNNING运行状态-1<<29表示线程池可以接新的任务并且处理队列任务

SHUTDOWN关闭状态态-1<<29表示线程池不接受新的线程池任务但是可以处理队列中的任务

STOP停止状态1<<29表示线程池不接受新的线程池任务也不处理队列中的任务并且中断线程池里中正在执行的任务

TIDYING2<<29表示所有的线程池都已经中断了,线程数为0,线程状态转为为TIDYING,将执行terminated钩子函数

TERMINATED3<<29表示所有terminated方法都已经执行完成。

线程状态之间装换图

线程池的提交任务执行流程首先我们来看平时业务代码是提交任务到线程池执行的函数是通过execute或者submit方法,区别就是submit返回具有Future,execute返回void,的、那么接下来我们主要分析execute的执行流程,submit涉及到线程异步返回,之后会另外单独分析,那么下面这个execute函数就能看出线程池的整个执行流程,

publicvoidexecute(Runnablecommand){if(command==null)thrownewNullPointerException();/**Proceedin3steps:**1.IffewerthancorePoolSizethreadsarerunning,tryto*startanewthreadwiththegivencommandasitsfirst*task.ThecalltoaddWorkeratomicallychecksrunStateand*workerCount,andsopreventsfalsealarmsthatwouldadd*threadswhenitshouldn't,byreturningfalse.**2.Ifataskcanbesuccessfullyqueued,thenwestillneed*todouble-checkwhetherweshouldhaveaddedathread*(becauseexistingonesdiedsincelastchecking)orthat*thepoolshutdownsinceentryintothismethod.Sowe*recheckstateandifnecessaryrollbacktheenqueuingif*stopped,orstartanewthreadiftherearenone.**3.Ifwecannotqueuetask,thenwetrytoaddanew*thread.Ifitfails,weknowweareshutdownorsaturated*andsorejectthetask.*/intc=ctl.get();if(workerCountOf(c)<corePoolSize){if(addWorker(command,true))return;c=ctl.get();}if(isRunning(c)&&workQueue.offer(command)){intrecheck=ctl.get();if(!isRunning(recheck)&&remove(command))reject(command);elseif(workerCountOf(recheck)==0)//当线程池的核心线程数设置为0情况下,那么这时workerCountOf(recheck)为0,这时就开启非线程数处理队列任务addWorker(null,false);}elseif(!addWorker(command,false))reject(command);}线程池执行任务流程图如下:

我相信大概的流程一般同学是清楚的:

当线程数的Worker线程<corePoolSize创建核心线程数执行

当线程数的Worker线程>corePoolSize,将任务加入任务队列中

则当corePoolSize<maxPoolsize,则新增非核心线程执行任务

当队列满了,线程数也已经达到maxPoolsize,则执行拒绝策略

实际源码中执行流程还有一些小细节容易被忽略的地点

重新检查线程的状态以及检查线程池的线程数的流程

线程池新增工作线程的流程线程池新增工作任务主要addWorker方法。由于代码比较长,我就在代码里写好注释

privatebooleanaddWorker(RunnablefirstTask,booleancore){retry:for(;;){intc=ctl.get();intrs=runStateOf(c);//Checkifqueueemptyonlyifnecessary.if(rs>=SHUTDOWN&&//第一个条件:线程至少不是运行状态,那么就是shutdownstoptidying,terminated状态!(rs==SHUTDOWN&&firstTask==null&&!workQueue.isEmpty()))//第二个条件:当前线程池是shutdown状态且任务队列非空并且工作任务第一个任务是空的取反条件,这个含义是当除了SHUTDOWN状态且第一个任务为空且任务队列不为空//情况下,直接返回false,增加Work线程失败returnfalse;for(;;){intwc=workerCountOf(c);if(wc>=CAPACITY||wc>=(core?corePoolSize:maximumPoolSize))returnfalse;if(compareAndIncrementWorkerCount(c))breakretry;c=ctl.get();//Re-readctlif(runStateOf(c)!=rs)continueretry;//elseCASfailedduetoworkerCountchange;retryinnerloop}}booleanworkerStarted=false;booleanworkerAdded=false;Workerw=null;try{w=newWorker(firstTask);finalThreadt=w.thread;if(t!=null){finalReentrantLockmainLock=this.mainLock;mainLock.lock();try{//Recheckwhileholdinglock.//BackoutonThreadFactoryfailureorif//shutdownbeforelockacquired.intrs=runStateOf(ctl.get());if(rs<SHUTDOWN||//线程池是running状态(rs==SHUTDOWN&&firstTask==null)){//线程池处于shutdown状态并且第一个task为空if(t.isAlive())//precheckthattisstartablethrownewIllegalThreadStateException();//加入工作线程的集合workers.add(w);ints=workers.size();if(s>largestPoolSize)//记录最大线程数largestPoolSize=s;workerAdded=true;}}finally{-mainLock.unlock();-}-if(workerAdded){-t.start();workerStarted=true;}}}finally{if(!workerStarted)addWorkerFailed(w);}returnworkerStarted;}添加工作线程主要步骤

检查线程池的运行状态以及队列是否是空,增加线程。为什么增加这个判断,主要是因为线程池是多线程的随便可能另外调用shutdown等方法关闭线程池,所以做每一步之前都要再次check线程池的状态,其中比较重要的点是线程池在除了Running状态,其他的只有shutdow状态,且队列任务非空的情况,才能增加work线程处理任务。

判断线程池的线程是核心线程数,然后就判断大于核心线程数,如果不是增加的核心线程数,然后通过CAS增加线程数加1,然后re-read的ctl的现在的状态是否刚开始进入循环的状态保持一致。

创建Worker对象,它的第一个参数Runable就是执行的第一个task,然后获取mainLock的重入锁,然后再次判断线程池的状态是否是shutdown状态,然后将Worker对象加入工作线程的Set集合中,判断是大于largePoolSize,则将workSet的size赋值largePoolSize,然后赋值workerAdded为true,接下来在finnally中workerAdded为true,则调用Worker的start方法启动该Worker线程,

如果WorkerAdded失败,则从Worder的Set移除刚才加入Worker线程,并将线程池的线程数减1,

工作线程Worker的执行流程首先来看下Work的类的成员变量的构造函数,从下面的Work的代码,可以看到它是实现了RUnnable接口,上一节Worker启动是调用了它的start方法,真正由操作系统调度执行的其run方法,那么接下来重点看下run的工作流程。

privatefinalclassWorkerextendsAbstractQueuedSynchronizerimplementsRunnable{/***Thisclasswillneverbeserialized,butweprovidea*serialVersionUIDtosuppressajavacwarning.*/privatestaticfinallongserialVersionUID=6138294804551838833L;/**Threadthisworkerisrunningin.Nulliffactoryfails.*/finalThreadthread;/**Initialtasktorun.Possiblynull.*/RunnablefirstTask;/**Per-threadtaskcounter*/volatilelongcompletedTasks;/***CreateswithgivenfirsttaskandthreadfromThreadFactory.*@paramfirstTaskthefirsttask(nullifnone)*/Worker(RunnablefirstTask){//初始化状态为-1,表示不能被中断setState(-1);//inhibitinterruptsuntilrunWorkerthis.firstTask=firstTask;this.thread=getThreadFactory().newThread(this);}下面代码中Work的run直接调用runWork,并传入自身对象,开始一个循环判断第一个任务后者从任务队列中取任务不为空,就开始上锁,然后执行任务,如果任务队列为空了,则处理Work的退出。

/**DelegatesmainrunlooptoouterrunWorker*/publicvoidrun(){//直接调用runWorker函数runWorker(this);}finalvoidrunWorker(Workerw){//Wokder当前线程Threadwt=Thread.currentThread();Runnabletask=w.firstTask;w.firstTask=null;//将state值赋值为0,这样就运行中断w.unlock();//allowinterruptsbooleancompletedAbruptly=true;try{//循环判断第一个Task获取从获取任务while(task!=null||(task=getTask())!=null){//获取当前Work的锁,处理任务,也就是当前Work线程处理是同步处理任务的w.lock();//Ifpoolisstopping,ensurethreadisinterrupted;//ifnot,ensurethreadisnotinterrupted.This//requiresarecheckinsecondcasetodealwith//shutdownNowracewhileclearinginterrupt//线程池的状态至少是stop,即使stop,tidying.terminated状态if((runStateAtLeast(ctl.get(),STOP)//检查线程是否中断且清楚中断||(Thread.interrupted()&&//再次检查线程池的状态至少是STOPrunStateAtLeast(ctl.get(),STOP)))&&//再次判断是否中断!wt.isInterrupted())//中断线程wt.interrupt();try{//执行业务任务前处理(钩子函数)beforeExecute(wt,task);Throwablethrown=null;try{//这里就是执行提交线程池的Runnable的任务的run方法task.run();}catch(RuntimeExceptionx){thrown=x;throwx;}catch(Errorx){thrown=x;throwx;}catch(Throwablex){thrown=x;thrownewError(x);}finally{//执行业务任务后处理(钩子函数)afterExecute(task,thrown);}}finally{//执行结束重置为空,回到while循环拿下一个task=null;//处理任务加1w.completedTasks++;//释放锁,处理下一个任务w.unlock();}}//代码执行到这里,代表业务的任务没有异常,不然不会走到这里,//因为上一层try没有catch异常的,而业务执行出现异常,最里层//虽然catch了异常,但是也都通过throw向外抛出completedAbruptly=false;}finally{//如果循环结束,则处理Work退出工作,代表任务拿不到任务,即任务队列没有任务了processWorkerExit(w,completedAbruptly);}}下面就来看下getTask获取任务队列的处理逻辑、如果这里返回null,即runWorker循环退出,则会处理finnaly中processWorkExit,处理Work线程的退出,下面是getWork返回null的情况:

如果线程池状态值至少是SHUTDOWN状态,并且线程池状态值至少是STOP状态,或者是任务队列是空,则将线程池的workcout减1,并返回null,

计算线程池中线程池的数量,如果线程数量大于最大线程数量,或者allowCoreThreadTimeOut参数为true或者线程数大于并且任务队列为空,则将线程池减1,并返回null,

privateRunnablegetTask(){//超时标志booleantimedOut=false;//Didthelastpoll()timeout?for(;;){//获取线程状态intc=ctl.get();//线程状态intrs=runStateOf(c);//Checkifqueueemptyonlyifnecessary.//如果线程池状态值至少是SHUTDOWN状态,if(rs>=SHUTDOWN线程池状态值至少是STOP状态,或者是任务队列是空&&(rs>=STOP||workQueue.isEmpty())){//CAS将worker线程数减1decrementWorkerCount();returnnull;}//计算线程池线程数量intwc=workerCountOf(c);//Areworkerssubjecttoculling?//allowCoreThreadTimeOut参数设置为true,或则线程池的线程数大于corePoolSize,表示需要超时的Worker需要退出,booleantimed=allowCoreThreadTimeOut||wc>corePoolSize;//线程数大于最大线程数||已经超时if((wc>maximumPoolSize||(timed&&timedOut))//线程数大于1或者任务队列为空&&(wc>1||workQueue.isEmpty())){//CAS将线程数减1if(compareAndDecrementWorkerCount(c))returnnull;continue;}try{//需要处理超时的Worker,则获取任务队列中任务等待的时间//就是线程池构造函数中keepAliveTime时间,如果不处理超时的Worker//则直接调用take一直阻塞等待任务队列中有任务,拿到就返回Runnale任务Runnabler=timed?workQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS):workQueue.take();if(r!=null)returnr;timedOut=true;}catch(InterruptedExceptionretry){timedOut=false;}}}Worker的退出处理:1从上面分析知道completedAbruptly是任务执行时是否出现异常标志,如果任务执行过程出错,则将线程池的线程数量减12.加线程池的mainLock的全局锁,这里主要区分Worker执行任务中,拿的是Worker内部的锁,完成任务加1,将worker从Worker的集合移除,3.执行tryTerminate函数,是否线程池线程池是否关闭4.根据线程池状态是否补充非核心的Worker线程去处理

privatevoidprocessWorkerExit(Workerw,booleancompletedAbruptly){//任务执行时出现异常,则减去工作if(completedAbruptly)//Ifabrupt,thenworkerCountwasn'tadjusteddecrementWorkerCount();//拿到线程池的主锁finalReentrantLockmainLock=this.mainLock;//加锁mainLock.lock();try{//完成任务加1completedTaskCount+=w.completedTasks;//将worker从Worker的集合移除workers.remove(w);}finally{mainLock.unlock();}//尝试线程池关闭tryTerminate();//获取线程池的ctlintc=ctl.get();//如果线程池的状态值小于STOP,即使SHUTDOWNRUNNINGif(runStateLessThan(c,STOP)){//任务执行没有异常if(!completedAbruptly){//allowCoreThreadTimeOut参数true,则min=0,表示不需要线程常驻。//负责是有corePoolSize个线程常驻线程池intmin=allowCoreThreadTimeOut?0:corePoolSize;if(min==0&&!workQueue.isEmpty())min=1;//如果线程池数大于最小,也就是不需要补充线程执行任务队列的任务if(workerCountOf(c)>=min)return;//replacementnotneeded}//走到这里表示线程池的线程数为0,而任务队列又不为空,得补充一个线程处理任务addWorker(null,false);}}t

JavaSwing图形界面开发与案例详解的作品目录

第1章JavaSwing概述

1.1什么是JavaSwing

1.1.1Swing的发展史

1.1.2Swing的功能

1.1.3Swing的特性

1.2.1avaSwing的包结构

1.3一个JavaSwing程序实例

1.4本章小结

1.5本章习题

第2章如何使用IDE开发Swing程序

2.1如何利用Eclipse开发Swing程序

2.2如何利用JBuilder开发Swing程序

2.3如何利用NetBeans开发Swing程序

2.4本章小结

2.5本章习题

第3章JavaSwirlg组件基础

3.1Swing组件类的层次

3.2Window类

3.2.1顶层容器类和包含层次

3.2.2在顶层容器中添加组件

3.2.3在顶层容器中添加菜单栏

3.3JComponent类

3.4本章小结

3.5本章习题

第4章如何使用标签和按钮组件

4.1如何使用标签

4.2如何使用按钮

4.2.1如何使用普通按钮

4.2.2如何使用单选按钮

4.2.3如何使用复选框

4.2.4按钮组件的实例应用

4.3本章小结

4.4本章习题

第5章如何使用布局管理器组件

5.1布局管理器概述

5.2布局管理器的种类

5.2.1BorderLayout

5.2.2FlowLayout

5.2.3GridLayout

5.2.4GridBagLayout

5.2.5CardLayout

5.2.6BoxLayout

5.2.7SpringLayout

5.2.8GroupLayout

5.3自定义布局管理器的创建

5.4本章小结

5.5本章习题

第6章如何使用面板组件

6.1如何使用JPanel

6.2如何使用JScrollPane

6.3如何使用JSplitPane

6.4如何使用JTabbedPane

6.5如何使用JIntemalFrame

6.6如何使用JLayeredPane

6.7如何使用JRootPane

6.8本章小结

6.9本章习题

第7章Swirlg事件处理机制

7.1Swing事件处理机制概述

7.2Swing中的监听器

7.2.1事件处理的过程与步骤

7.2.2匿名类方式处理事件

7.2.3适配器类

7.2.4Swing所支持的事件监听器

7.2.5窗口事件的处理

7.2.6动作事件的处理

7.2.7焦点事件的处理

7.3本章小结

7.4本章习题

第8章如何使用列表框和下拉列表框组件

8.1如何使用列表框JList

8.1.1使用数组方式创建列表框

8.1.2使用Vector方式创建列表框

8.1.3使用ListModel方式创建列表框

8.1.4列表框选取事件的处理

8.1.5列表框双击事件的处理

8.2如何使用下拉列表框JComboBox

8.2.1使用数组和Vector创建下拉列表框

8.2.2使用ComboBoxModel创建下拉列表框

8.2.3下拉列表框的事件处理

8.3本章小结

8.4本章习题

第9章如何使用进度条、时间、滑块和分隔条组件

9.1如何使用进度条组件JProgressBar

9.2如何使用时间组件Timer

9.3如何使用滑块组件JSlider

9.4如何使用分隔条组件JSeparator

9.5本章小结

9.6本章习题

第10章如何使用选取器组件

10.1如何使用文件选取器JFileChooser

10.1.1如何创建JFileChooser组件

10.1.2如何创建JFileChooser对话框

10.2如何使用颜色选取器JColorChooser

10.3本章小结

10.4本章习题

第11章如何使用文本组件

11.1文本组件概述

11.2如何使用普通文本组件

11.2.1如何使用JTextField

11.2.2如何使用JPasswordField

11.2.3如何使用JFormattedTextField

11.3如何使用文本区组件

11.4如何打印文本组件

11.5本章小结

11.6本章习题

第12章如何使用窗口、对话框和JApplet组件

12.1如何使用窗口组件

12.2如何使用对话框组件

12.3如何使用JApplet组件

12.4本章小结

12.5本章习题

第13章如何使用菜单和工具条组件

13.1如何使用菜单组件

13.1.1菜单组件的类层次

13.1.2如何创建菜单

13.1.3如何处理菜单事件

13.1.4如何响应键盘操作

13.1.5如何使用弹出式菜单

13.1.6如何使用菜单项的启用和禁用功能

13.1.7如何创建复选框菜单项

13.1.8如何创建单选按钮菜单项

13.1.9如何定义个性化菜单

13.1.10菜单组件的常用API

13.2如何使用工具条组件

13.2.1如何创建工具条

13.2.2如何定义个性化工具条

13.2.3工具条组件的常用API

13.3本章小结

13.4本章习题

第14章如何使用表格组件

14.1如何创建一个表格

14.2如何把表格加入容器

14.3如何设置表格列宽

14.4如何创建表格模型

14.5如何监听数据变化

14.6如何使用选择器

14.7如何使用编辑器和渲染器

14.8如何使用自定义渲染器

14.9如何为单元格指定文字说明

14.10如何为表头指定文字说明

14.11如何使用排序和过滤

14.12如何使用组合框作为编辑器

14.13如何使用其他编辑器

14.14如何使用编辑器验证文本

14.15如何打印表格

14.16本章小结

14.17本章习题

第15章如何使用树组件

15.1如何创建树

15.2如何创建数据模型

15.3如何处理节点事件

15.3.1如何处理TreeModelEvent事件

15.3.2如何处理TreeSelectionEvent事件

15.4如何定义个性化树

15.5树组件的常用API

15.6本章小结

15.7本章习题

第16章如何使用Swing观感器

16.1如何设置程序的观感

16.2如何自定义观感器

16.3本章小结

16.4本章习题

第17章Swing与并发

17.1多线程问题

17.2初始线程

17.3事件分派线程

17.4工作线程

17.4.1简单的背景任务

17.4.2拥有临时结果的任务

17.4.3取消背景任务

17.4.4绑定属性和状态方法

17.5本章小结

17.6本章习题

第18章Swing模型架构

18.1传统的MVC设计模式

18.2可分离的模型架构

18.3本章小结

18.4本章习题

第19章Swing的其他特性

19.1如何在Swing组件中使用HTML

19.2如何使用边框

19.2.1如何使用Swing中的边框

19.2.2如何创建自定义边框

19.2.3边框组件的常用API

19.3如何使用图标

19.4如何使用动作

19.5如何支持辅助技术

19.6如何使用焦点子系统

19.7如何使用键绑定

19.8如何在对话框中使用Modality

19.9如何创建SplashScreen

19.10如何使用SystemTray

19.11如何使用Swing拖曳功能和数据传输

19.12本章小结

19.13本章习题

第20章Swirlg实现通讯录系统

20.1通讯录系统的软件框架

20.2通讯录系统的登录系统

20.3通讯录系统的主菜单系统

20.3.1数据库模块的设计

20.3.2信息界面模块的设计

20.3.3功能模块的设计

20.3.4其他模块的设计

20.3.5TabbedPane容器框架的设计

20.3.6主菜单的设计

20.4本章小结

……

OK,本文到此结束,希望对大家有所帮助。

java运行环境安装及配置教程(我的世界java运行环境)ai智能推荐志愿免费,2022智能AI填报志愿软件 比较精准的APP有哪些