设计模式 - 单例模式之多线程调试与破坏单例

前言

在之前的 设计模式 - 单例模式(详解)看看和你理解的是否一样? 一文中,我们提到了通过Idea 开发工具进行多线程调试、单例模式的暴力破坏的问题;由于篇幅原因,现在单独开一篇文章进行演示:线程不安全的单例在多线程情况下为何被创建多个、如何破坏单例。

如果还不知道如何使用IDEA工具进行线程模式的调试,请先阅读我之前发的一篇文章: 你不知道的 IDEA Debug调试小技巧

一、线程不安全的单例在多线程情况下为何被创建多个

首先回顾简单线程不安全的懒汉式单例的代码以及测试程序代码:

/**
 * @author eamon.zhang
 * @date 2019-09-30 上午10:55
 */
public class LazySimpleSingleton {
    private LazySimpleSingleton(){}
    private static LazySimpleSingleton instance = null;

    public static LazySimpleSingleton getInstance(){
        if (instance == null) {
            instance = new LazySimpleSingleton();
        }
        return instance;
    }
}

// 测试程序
@Test
public void test() {
    try {
        ConcurrentExecutor.execute(() -> {
            LazySimpleSingleton instance = LazySimpleSingleton.getInstance();
            System.out.println(Thread.currentThread().getName()   " : "   instance);
        }, 2, 2);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

对于这个单例,我们毫无疑问认为它是线程不安全的,至于为什么,接下来使用IDEA工具的线程debug模式来直观的找出答案。

在关键代码上打断点

  1. 单例类LazySimpleSingletonif (instance == null) 处:

  1. 测试类,多线程入口调用getInstance()处:

开始调试

  1. 启动 debug ,我们可以在调试窗口找到我们启动的线程:

  1. pool-1-thread-1 线程单步执行到if (instance == null) 断点处,观察instance值为null

  1. pool-1-thread-1执行到instance = new LazySimpleSingleton();处等待初始化:

  1. 切换线程 pool-1-thread-2 同样单步执行到 if (instance == null) 断点处,此时观察instance值也为null(这就是我们常说的两个线程同时执行到断代码处):

  1. 同样将pool-1-thread-2执行到instance = new LazySimpleSingleton();处等待初始化:

  1. 显然,这两个线程都满足if (instance == null) 的条件,都应该到对应的代码块中执行实例化操作,那么这两个线程就会分别初始化:

线程 pool-1-thread-1 实例化后:

切换线程 pool-1-thread-2 观察 instance 值已经被初始化了,但是,线程pool-1-thread-2 还是会被实例化一遍:

线程pool-1-thread-2实例化后:

大家是否一目了然了呢?

  1. 将两个线程执行完,看控制台:

大家可以看到,虽然输出打印的对象是同一个,但是,确实是创建了两遍,只不过 pool-1-thread-2 实例化后将 pool-1-thread-1实例化的对象值给覆盖了。

当我将线程pool-1-thread-1和线程pool-1-thread-2同时执行到instance = new LazySimpleSingleton();处然后先让pool-1-thread-1执行完打印后,再将pool-1-thread-2执行实例化操作,就会看到打印的对象会是不一样的了:

这就是通过线程调试模式手动控制线程执行顺序来模拟还原多线程环境下,线程不安全的情况。


二、改进线程不安全的单例

我们明白了线程不安全的原因是两个线程同时拿到的instance资源都为null,从而都进行实例化。那么有没有什么方法能解决呢?当然有,给 getInstance()加 上 synchronized 关键字,使这个方法变成线程同步方法:

public class LazySimpleSingleton {
    private LazySimpleSingleton(){}
    private static LazySimpleSingleton instance = null;

    public synchronized static LazySimpleSingleton getInstance(){
        if (instance == null) {
            instance = new LazySimpleSingleton();
        }
        return instance;
    }
}

当我们将其中一个线程执行并调用 getInstance()方法时,另一个线程在调用 getInstance()方法,线程的状态由 RUNNING 变成了MONITOR,出现阻塞。直到第一个线程执行完,第二个线程才恢复 RUNNING 状态继续调用 getInstance() 方法

这就解决了之前所说的线程安全问题,但是这样子在线程数量比较多情况下,如果 CPU分配压力上升,会导致大批量线程出现阻塞,从而导致程序运行性能大幅下降;为了解决线程安全和程序性能问题,于是乎有了我们的双重检查式的单例。这里就不再多说了。


三、破坏单例

一般情况下,我们创建使用饿汉式单例或双重检查的懒汉式单例是没有问题的,但是在一定情况下,会发生单例被破坏。

反射破坏单例

实际情况下,公司一个程序员写了一个单例,但是另外一个程序员,可能比较牛 X,写代码风格有点不一样,他通过反射来调用别人写的接口,这就会出现此单例并非彼单例的情况。这就破坏了单例。

演示

在我们写单例的时候,大家有没有注意到私有的构造方法前面的修饰符仅为 private,如果我们使用反射来调用其构造方法,然后,再调用 getInstance()方法,应该就会有两个不同的实例。

我们以前面说单例的文章中的 LazyInnerClassSingleton为例,编写反射调用测试代码:

@Test
public void testReflex() {
    try {
        // 很无聊的情况下,进行破坏
        Class<LazyInnerClassSingleton> clazz = LazyInnerClassSingleton.class;
        // 通过反射拿到私有的构造方法
        Constructor<LazyInnerClassSingleton> c = clazz.getDeclaredConstructor(null);
        // 设置访问属性,强制访问
        c.setAccessible(true);

        // 暴力初始化两次,这就相当于调用了两次构造方法
        LazyInnerClassSingleton o1 = c.newInstance();
        LazyInnerClassSingleton o2 = c.newInstance();
        // 只要 o1和o2 地址不相等,就可以说明这是两个不同的对象,也就是违背了单例模式的初衷
        System.out.println(o1 == o2);
    } catch (Exception e) {
        e.printStackTrace();
    }

}

运行结果如下:

显然,是创建了两个不同的实例。现在,我们在其构造方法中做一些限制,一旦出现多次重复创建,则直接抛出异常。来看优化后的代码:

public class LazyInnerClassSingleton {

    private LazyInnerClassSingleton() {
        if(LazyHolder.INSTANCE != null){
            throw new RuntimeException("不允许创建多个实例");
        }
    }

    // 注意关键字final,保证方法不被重写和重载
    public static final LazyInnerClassSingleton getInstance() {
        return LazyHolder.INSTANCE;
    }

    private static class LazyHolder {
        // 注意 final 关键字(保证不被修改)
        private static final LazyInnerClassSingleton INSTANCE = new LazyInnerClassSingleton();
    }
}

再次调用:

至此,就避免了单例被反射破坏的问题。

序列化破坏单例

另外一种情况,可能会遇到,我们需要将对象序列化到磁盘,下次使用时再从磁盘反序列化回来,反序列化的对象会被重新分配内存,那如果序列化的对象为单例,则就违背了单例模式的初衷。这也相当于破坏了单例。

演示

我们还是以LazyInnerClassSingleton为例,将LazyInnerClassSingleton 实现 Serializable 接口;

然后编写测试代码:

/**
 * @author eamon.zhang
 * @date 2019-10-08 下午3:06
 */
public class SerializableTest {
    public static void main(String[] args) {
        LazyInnerClassSingleton s1 = null;
        LazyInnerClassSingleton s2 = LazyInnerClassSingleton.getInstance();

        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream("LazyInnerClassSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("LazyInnerClassSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (LazyInnerClassSingleton)ois.readObject();
            ois.close();

            System.out.println(s1);
            System.out.println(s2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

执行测试代码:

可以看到,结果为两个不同的对象。这同样违背了单例模式的初衷。那么我们如何保证序列化的情况也能实现单例呢?其实也很简单,使用 readResolve() 方法即可:

public class LazyInnerClassSingleton implements Serializable {

    private LazyInnerClassSingleton() {
        if (LazyHolder.INSTANCE != null) {
            throw new RuntimeException("不允许创建多个实例");
        }
    }

    // 注意关键字final,保证方法不被重写和重载
    public static final LazyInnerClassSingleton getInstance() {
        return LazyHolder.INSTANCE;
    }

    private static class LazyHolder {
        // 注意 final 关键字(保证不被修改)
        private static final LazyInnerClassSingleton INSTANCE = new LazyInnerClassSingleton();
    }

    // 解决反序列化对象不一致问题
    private Object readResolve() {
        return LazyHolder.INSTANCE;
    }
}

大家肯定会问,why?

为了一探究竟,我们来看一下 JDK 源码,我们进入 ObjectInputStream 类的 readObject()方法:

public final Object readObject() throws IOException, ClassNotFoundException {
        if (this.enableOverride) {
            return this.readObjectOverride();
        } else {
            int outerHandle = this.passHandle;

            Object var4;
            try {
                Object obj = this.readObject0(false);
                this.handles.markDependency(outerHandle, this.passHandle);
                ClassNotFoundException ex = this.handles.lookupException(this.passHandle);
                if (ex != null) {
                    throw ex;
                }

                if (this.depth == 0L) {
                    this.vlist.doCallbacks();
                    this.freeze();
                }

                var4 = obj;
            } finally {
                this.passHandle = outerHandle;
                if (this.closed && this.depth == 0L) {
                    this.clear();
                }

            }

            return var4;
        }
    }

我们发现:readObject 中又调用了我们重写的 readObject0()方法,进入 readObject0()方法:

private Object readObject0(boolean unshared) throws IOException {
        ...
        try {
            switch(tc) {
            ...
            case 115:
                var4 = this.checkResolve(this.readOrdinaryObject(unshared));
                return var4;
            ...
        } finally {
            --this.depth;
            this.bin.setBlockDataMode(oldMode);
        }

        return var4;
    }

我们看到代码中调用了 ObjectInputStreamreadOrdinaryObject() 方法,我们继续进入看源码:

private Object readOrdinaryObject(boolean unshared) throws IOException {
        ...
            if (cl != String.class && cl != Class.class && cl != ObjectStreamClass.class) {
                Object obj;
                try {
                    obj = desc.isInstantiable() ? desc.newInstance() : null;
                } catch (Exception var7) {
                    throw (IOException)(new InvalidClassException(desc.forClass().getName(), "unable to create instance")).initCause(var7);
                }

        ...

        }
    }

发现调用了 ObjectStreamClassisInstantiable()方法,而 isInstantiable()里面的代码如下:

boolean isInstantiable() {
    this.requireInitialized();
    return this.cons != null;
}

代码非常简单,就是判断一下构造方法是否为空,构造方法不为空就返回 true,也就是说,只要有无参构造方法就会实例化;这时候,其实还没有找到为什么加上readResolve()方法就避免了单例被破坏的真正原因,我们再次回到ObjectInputStreamreadOrdinaryObject()方法继续往下看可以找到如下代码:

private Object readOrdinaryObject(boolean unshared) throws IOException {
    ...
    if (obj != null && this.handles.lookupException(this.passHandle) == null && desc.hasReadResolveMethod()) {
        Object rep = desc.invokeReadResolve(obj);
        if (unshared && rep.getClass().isArray()) {
            rep = cloneArray(rep);
        }

        if (rep != obj) {
            if (rep != null) {
                if (rep.getClass().isArray()) {
                    this.filterCheck(rep.getClass(), Array.getLength(rep));
                } else {
                    this.filterCheck(rep.getClass(), -1);
                }
            }

            obj = rep;
            this.handles.setObject(this.passHandle, rep);
        }
    }
    ...
}

判断无参构造方法是否存在之后,又调用了 hasReadResolveMethod()方法:

boolean hasReadResolveMethod() {
    this.requireInitialized();
    return this.readResolveMethod != null;
}

逻辑非常简单,就是判断readResolveMethod 是否为空,不为空就返回 true。那么 readResolveMethod是在哪里赋值的呢? 通过全局查找找到了赋值代码在私有方法 ObjectStreamClass()方法中给 readResolveMethod 进行赋值,来看代码:

 ObjectStreamClass.this.readResolveMethod = ObjectStreamClass.getInheritableMethod(cl, "readResolve", (Class[])null, Object.class);

代码的逻辑其实就是通过反射找到一个无参的 readResolve()方法,并且保存下来,现在再回到 ObjectInputStreamreadOrdinaryObject() 方法继续往下看,如果readResolve()存在则调用 invokeReadResolve()方法:

Object invokeReadResolve(Object obj) throws IOException, UnsupportedOperationException {
    this.requireInitialized();
    if (this.readResolveMethod != null) {
        try {
            return this.readResolveMethod.invoke(obj, (Object[])null);
        } catch (InvocationTargetException var4) {
            Throwable th = var4.getTargetException();
            if (th instanceof ObjectStreamException) {
                throw (ObjectStreamException)th;
            } else {
                throwMiscException(th);
                throw new InternalError(th);
            }
        } catch (IllegalAccessException var5) {
            throw new InternalError(var5);
        }
    } else {
        throw new UnsupportedOperationException();
    }
}

我们可以看到在 invokeReadResolve()方法中用反射调用了 readResolveMethod() 方法。 通过JDK源码分析我们可以看出,虽然,增加 readResolve()方法返回实例,解决了单例被破坏的问题。但是,我们通过分析源码以及调试,我们可以看到实际上实例化了两 次,只不过新创建的对象没有被返回而已.

那如果,创建对象的动作发生频率增大,就 意味着内存分配开销也就随之增大;为了解决这个问题,我们推荐使用注册式单例。

为何建议使用注册式(枚举式)单例

我们在前文中说到了,我们极力推荐使用枚举类型的单例;接下来我们分析一下原因:

使用 Java 反编译工具 Jad(自行下载),解压后,使用命令行调用:

./jad ~/IdeaProjects/own/java-advanced/01.DesignPatterns/design-patterns/build/classes/java/main/com/eamon/javadesignpatterns/singleton/enums/EnumSingleton.class

会在当前目录生成一个 EnumSingleton.jad文件,我们使用 vscode 打开这个文件查看:

public final class EnumSingleton extends Enum
{

    public static EnumSingleton[] values()
    {
        return (EnumSingleton[])$VALUES.clone();
    }

    public static EnumSingleton valueOf(String name)
    {
        return (EnumSingleton)Enum.valueOf(com/eamon/javadesignpatterns/singleton/enums/EnumSingleton, name);
    }

    private EnumSingleton(String s, int i)
    {
        super(s, i);
        instance = new EnumResource();
    }

    public Object getInstance()
    {
        return instance;
    }

    public static final EnumSingleton INSTANCE;
    private Object instance;
    private static final EnumSingleton $VALUES[];

    static
    {
        INSTANCE = new EnumSingleton("INSTANCE", 0);
        $VALUES = (new EnumSingleton[] {
            INSTANCE
        });
    }
}

请注意这段代码:

static
{
    INSTANCE = new EnumSingleton("INSTANCE", 0);
    $VALUES = (new EnumSingleton[] {
        INSTANCE
    });
}

原来枚举类单例在静态代码块中就给INSTANCE 赋了值,是饿汉式单例的实现方式。那么同样的,我们能否通过反射和序列化方式进行破坏呢?

先分析通过序列化方式:

我们还是回到JDK源码:在 ObjectInputStreamreadObject0()方法中有如下代码:

 private Object readObject0(boolean unshared) throws IOException {
    ...
        case 126:
            var4 = this.checkResolve(this.readEnum(unshared));
    ...

    return var4;
}

我们看到 readObject0()中调用了readEnum()方法,跟进该方法:

private Enum<?> readEnum(boolean unshared) throws IOException {
    if (this.bin.readByte() != 126) {
        throw new InternalError();
    } else {
        ObjectStreamClass desc = this.readClassDesc(false);
        if (!desc.isEnum()) {
            throw new InvalidClassException("non-enum class: "   desc);
        } else {
            int enumHandle = this.handles.assign(unshared ? unsharedMarker : null);
            ClassNotFoundException resolveEx = desc.getResolveException();
            if (resolveEx != null) {
                this.handles.markException(enumHandle, resolveEx);
            }

            String name = this.readString(false);
            Enum<?> result = null;
            Class<?> cl = desc.forClass();
            if (cl != null) {
                try {
                    Enum<?> en = Enum.valueOf(cl, name);
                    result = en;
                } catch (IllegalArgumentException var9) {
                    throw (IOException)(new InvalidObjectException("enum constant "   name   " does not exist in "   cl)).initCause(var9);
                }

                if (!unshared) {
                    this.handles.setObject(enumHandle, result);
                }
            }

            this.handles.finish(enumHandle);
            this.passHandle = enumHandle;
            return result;
        }
    }
}

我们发现枚举类型其实通过类名和 Class 对象类找到一个唯一的枚举对象。因此,枚举对象不可能被类加载器加载多次。

那么是否可以通过反射进行破坏呢?我们先来执行以下反射破坏枚举类的测试代码:

@Test
public void testEnum(){
    try {
        // 很无聊的情况下,进行破坏
        Class<EnumSingleton> clazz = EnumSingleton.class;
        // 通过反射拿到私有的构造方法
        Constructor<EnumSingleton> c = clazz.getDeclaredConstructor(null);
        // 设置访问属性,强制访问
        c.setAccessible(true);

        // 暴力初始化两次,这就相当于调用了两次构造方法
        EnumSingleton o1 = c.newInstance();
        EnumSingleton o2 = c.newInstance();
        // 只要 o1和o2 地址不相等,就可以说明这是两个不同的对象,也就是违背了单例模式的初衷
        System.out.println(o1 == o2);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

执行结果:

报的是 java.lang.NoSuchMethodException 异常,意思是没找到无参的构造方法。

那么我们来看一下 java.lang.Enum 的源码,我们发现它只有一个protected的构造方法:

protected Enum(String name, int ordinal) {
    this.name = name;
    this.ordinal = ordinal;
}

那我们来做一个这样的测试:

@Test
public void testEnum1() {
    try {
        Class clazz = EnumSingleton.class;
        Constructor c = clazz.getDeclaredConstructor(String.class, int.class);
        c.setAccessible(true);
        EnumSingleton enumSingleton = (EnumSingleton) c.newInstance("Eamon", 666);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

发现控制台输出如下错误:

意思就是不能用反射来创建枚举类型。至于为什么,我们还是来看 JDK 源码,进入ConstructornewInstance()方法中:

    public T newInstance(Object... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        if (!this.override) {
            Class<?> caller = Reflection.getCallerClass();
            this.checkAccess(caller, this.clazz, this.clazz, this.modifiers);
        }

        if ((this.clazz.getModifiers() & 16384) != 0) {
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        } else {
            ConstructorAccessor ca = this.constructorAccessor;
            if (ca == null) {
                ca = this.acquireConstructorAccessor();
            }

            T inst = ca.newInstance(initargs);
            return inst;
        }
    }

原来,在源码中对枚举类型进行了强制性的判断(16384代表枚举类型),如果是枚举类型,直接抛异常。到此为止也就说明了为什么《Effective Java》推荐使用枚举来实现单例的原因: JDK 枚举的语法特殊性,以及反射也为枚举保驾护航,让枚举式单例成为一种比较优雅的实现。


本文中所涉及的源码可在 github 上找到,相关的测试代码在 test 包下:https://github.com/eamonzzz/java-advanced

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
【资源说明】 1、该资源包括项目的全部源码,下载可以直接使用! 2、本项目适合作为计算机、数学、电子信息等专业的课程设计、期末大作业和毕设项目,作为参考资料学习借鉴。 3、本资源作为“参考资料”如果需要实现其他功能,需要能看懂代码,并且热爱钻研,自行调试。 基于SSM架构实现的大型分布式购物网站-B2C项目源码+项目说明.zip # taotaoMalls 大型分布式购物网站-B2C项目(持续更新中) ##电商行业模式 - B2B:企业到企业、商家到商家。例如阿里巴巴。 - B2C:商家到客户。例如京东、淘宝商城 - C2C:客户到客户。闲鱼。 - O2O:线上到线下。美团、饿了么。 在互联网项目中尽可能的减少表的关联查询。 Sku:最小库存量单位。就是商品id,是商品最细粒度的划分,每一个sku都唯一对应一款商品,商品的颜色、配置。 ##SSM框架整合 ###dao层 - 1、配置数据源 - 2、让spring容器管理SqlSessionFactory,单例存在 - 3、把mapper的代理对象放到spring容器中。使用扫描包的方式加载mapper的代理对象。 ###Service层 - 1、事务管理 - 2、需要把service实现类放到spring容器中管理 ###表现层 - 1、配置注解驱动 - 2、配置视图解析器 - 3、需要扫描controller ###web.xml - 1、spring容器的配置 - 2、spring前端控制器的配置 - 3、post乱码过滤器 - 4、请求拦截 ###数据库连接池: Druid是目前最好的数据库连接池,在功能、性能、扩展性方面,都超过其他数据库连接池,包括DBCP、C3P0、BoneCP、Proxool、JBoss DataSource。 Druid已经在阿里巴巴部署了超过600个应用,经过多年多生产环境大规模部署的严苛考验。 ###配置静态资源映射: <mvc:resources location="/WEB-INF/js/" mapping="/js/**"/> <mvc:resources location="/WEB-INF/css/" mapping="/css/**"/> 子容器可以访问父容器中的对象。 ##分页插件pageHelper的使用 该分页插件对逆向工程生成的代码支持不好,不能对有查询条件的查询分页,会抛异常。 #图片保存位置 1、小型网站,传统项目是把图片放到Tomcat工程的image文件夹。 2、当并发增加后,就添加服务器,做tomcat集群。使用负载均衡服务器来决定存放到哪个服务器的image中。当图片传到tomcat1中到tomcat2中查找图片,我们可以将tomcat1和tomcat映射到另一台服务器上,然后做共享。或者在负载均衡中进行处理。 方案1:在负载均衡服务器上做一个session 映射,如果有记录则分发到原服务器上。 方案2:在负载均衡服务器中运行一个精灵线程,预测服务器压力过大时会自动把session转移压力过小的服务器中。 3、做专门的图片服务器。使用一个http服务器,Apache.或者Nginx。使用ftp服务上传图片,vsftpd ##图片服务器的搭建 使用centos7.0 需要把nginx的根目录指向ftp上传文件的目录。 ##service层 接收Controller传递过来的参数,一个文件MultiPartFile对象。把文件上传到ftp服务器。生成一个新的文件。 使用map实现,Map中的数据应该包含error。 ##Controller 接收页面传递过来的图片。调用service上传到图片服务器。返回结果。 参数:MultiPartFile uploadFile 返回值:返回json数据,应该返回一个pojo,PictureResult对象。 ##富文本编辑器 //同步文本框中的商品描述 itemAddEditor.sync(); Service接收商品的pojo,把商品数据写入到tb_item中,返回resultMap ##商品描述的保存 商品信息和商品描述是分开存储的。把商品信息描述保存到tb_item_desc表中。 ###规格参数 不同分类的规格参数不同,同一分类的规格项是相同的,值不同。 规格组: 规格项:规格值 同一类规格项的分组是相同的,规格参数个商品相关联。 ##实现方案: ###方案1:使用多表来存储 每一类商品有多个分组,每个分组下有多个项,每个商品对应不同的规格参数。 商品分类表:Tb_item_cat 一对多: 商品规格分组表:Tb_item_para
本书为中南大学精品教材立项项目,分为上下两篇共21章,涵盖了面向对象技术中Java开发环境配置、程序设计基础、面向对象原理以及UML的知识。本书使用的开发环境是JDK 1.6+Eclipse 3.3+Rational Rose 2003,逐步引领读者从基础到各个知识点进行学习。全书内容由浅入深,并辅以大量的实例说明,书本阶段性地提供了一些实验指导。 本书提供了所有实例的源代码以及开发过程中用到的软件下载地址,供读者学习参考使用。 本书为学校教学量身定做,供高校面向对象技术相关课程使用,对于缺乏项目实战经验的程序员来说可用于快速积累项目开发经验。 本书是中南大学精品教材建设中的一本特色教材,为高校计算机相关专业提供面向对象技术和UML的讲解。本书采用Java语言进行描述,针对Java技术标准编程进行详细的讲解,以简单通俗易懂的案例,逐步引领读者从基础到各个知识点进行学习。本书涵盖了JavaSE开发环境配置、程序设计基础、面向对象相关技术、常用API、UML基础知识。在章节中穿插了上机习题,并提供了答案,用于对该章内容进行阶段性总结演练。 作者长期从事教学工作,积累了丰富的经验,其“实战教学法”取得了很好的效果。本书适合教学。书中章节安排适当,将习题融于讲解的过程中,教师可以根据情况选用,也可以进行适当增减。 本书的知识体系结构如下所示,遵循了循序渐进的原则,逐步引领读者从基础到各个知识点进行学习。 上篇面向对象技术 第1章Java入门 第2章程序设计基础: 变量及其运算 第3章程序设计基础: 流程控制和数组 第4章实验指导1 第5章类、对象和成员 第6章封装 第7章继承和多态 第8章实验指导2 第9章异常处理 第10章Java常用API 第11章Java IO操作 第12章多线程开发 第13章反射技术 第14章实验指导3 下篇UML 第15章UML入门 第16章用例图 第17章类图和对象图 第18章实验指导4 第19章顺序图、协作图、状态图和活动图 第20章包图、构件图和部署图 第21章实验指导5 本书提供了全书所有实例的源代码,供读者学习参考使用,所有程序均经过了作者精心的调试。 由于时间仓促和作者水平有限,书中的错误和不妥之处敬请读者批评指正。 有关本书的意见反馈和咨询,读者可在清华大学出版社相关版块中与作者进行交流。 郭克华 2013年11月 目录 上篇面向对象技术 第1章Java入门 1.1认识Java 1.1.1认识编程语言 1.1.2Java的来历 1.1.3Java为什么流行 1.1.4Java的三个版本 1.1.5编程前的准备工作 1.2安装JDK 1.2.1获取JDK 1.2.2安装JDK步骤 1.2.3安装目录介绍 1.2.4环境变量设置 1.3开发第一个Java程序 1.3.1如何编写源代码 1.3.2如何将源代码编译成.class文件 1.3.3如何执行.class文件 1.3.4新手常见错误 1.4用Eclipse开发Java程序 1.4.1Eclipse的概念 1.4.2安装Eclipse 1.4.3如何建立项目 1.4.4如何开发Java程序 1.4.5如何维护项目 1.5小结 第2章程序设计基础:变量及其运算 2.1认识变量 2.1.1变量的定义 2.1.2变量有哪些类型 2.2如何使用变量 2.2.1如何使用整型变量 2.2.2如何使用浮点型变量 2.2.3如何使用字符型变量 2.2.4如何使用布尔型变量 2.2.5基本数据类型之间的类型转换 2.2.6基本数据类型和字符串之间的转换 2.2.7变量的作用范围 2.3注释的书写 2.4Java中的运算 2.4.1算术运算 2.4.2赋值运算 2.4.3关系运算 2.4.4逻辑运算 2.4.5运算符的优先级 2.5小结 第3章程序设计基础:流程控制和数组 3.1判断结构 3.1.1为什么需要判断结构 3.1.2if结构 3.1.3switch结构 3.2认识循环结构 3.2.1为什么需要循环结构 3.2.2while循环 3.2.3dowhile循环 3.2.4for循环 3.2.5循环嵌套 3.2.6break和continue 3.3数组 3.3.1为什么需要数组 3.3.2如何定义数组 3.3.3如何使用数组 3.3.4数组的引用性质 3.3.5数组的应用 3.3.6多维数组 3.4小结 第4章实验指导1 4.1关于变量和数据类型的实践 4.2流程控制和数组的综合实践 第5章类、对象和成员 5.1认识类和对象 5.1.1为什么需要类 5.1.2如何定义类 5.1.3如何使用类实例化对象 5.1.4如何访问对象中的成员变量 5.1.5对象的引用性质 5.2认识成员函数 5.2.1为什么需要函数 5.2.2如何定义和使用成员函数 5.2.3函数参数的传递 5.2.4认识函数重载 5.3认识构造函数 5.3.1为什么需要构造函数 5.3.2如何定义和使用构造函数 5.4静态变量和静态函数 5.4.1为什么需要静态变量 5.4.2静态变量的常见应用 5.4.3认识静态函数 5.4.4静态代码块 5.5小结 第6章封装 6.1使用封装 6.1.1为什么需要封装 6.1.2如何实现封装 6.2使用包 6.2.1为什么需要包 6.2.2如何将类放在包中 6.2.3如何访问包中的类 6.3使用访问控制修饰符 6.3.1什么是访问控制修饰符 6.3.2类的访问控制修饰符 6.3.3成员的访问控制修饰符 6.4使用类中类 6.5小结 第7章继承和多态 7.1使用继承 7.1.1为什么需要继承 7.1.2如何实现继承 7.1.3继承的底层本质 7.2成员的覆盖 7.2.1什么是成员覆盖 7.2.2成员覆盖有何作用 7.3使用多态性 7.3.1什么是多态 7.3.2如何使用多态性 7.3.3父子类对象的类型转换 7.4抽象类和接口 7.4.1为什么需要抽象类 7.4.2为什么需要接口 7.5其他内容 7.5.1final关键字 7.5.2Object类 7.6一些工具的使用 7.6.1将字节码打包发布 7.6.2文档的使用 7.7小结 第8章实验指导2 8.1单例模式的设计 8.1.1需求简介 8.1.2不用单例模式的效果 8.1.3最原始的单例模式 8.1.4首次改进 8.1.5再次改进 8.1.6思考题 8.2利用继承和多态扩充程序功能 8.2.1需求简介 8.2.2实现方法 8.2.3出现的问题 8.2.4改进 8.2.5测试 第9章异常处理 9.1认识异常 9.1.1生活中的异常 9.1.2软件中的异常 9.1.3为什么要处理异常 9.1.4异常机理 9.1.5常见异常 9.2异常的就地捕获 9.2.1为什么要就地捕获 9.2.2如何就地捕获异常 9.2.3如何捕获多种异常 9.2.4用finally保证安全性 9.3异常的向前抛出 9.3.1为什么要向前抛出 9.3.2如何向前抛出 9.4自定义异常 9.4.1为什么需要自定义异常 9.4.2如何自定义异常 9.5小结 第10章Java常用API 10.1数值运算 10.1.1用Math类实现数值运算 10.1.2实现随机数 10.2用String类进行字符串处理 10.3用StringBuffer类进行字符串处理 10.4基本数据类型的包装类 10.4.1认识包装类 10.4.2通过包装类进行数据转换 10.5认识Java集合 10.5.1为什么需要集合 10.5.2Java中的集合 10.6使用一维集合 10.6.1认识一维集合 10.6.2使用List集合 10.6.3使用Set集合 10.6.4使用Collections类对集合进行处理 10.6.5使用泛型简化集合操作 10.7Java中的二维集合 10.7.1使用Map集合 10.7.2使用Hashtable和Properties 10.8小结 第11章Java IO操作 11.1认识IO操作 11.2用File类操作文件 11.2.1认识File类 11.2.2使用File类操作文件 11.2.3使用File类操作目录 11.3字节流的输入输出 11.3.1认识字节流 11.3.2如何读写文件 11.3.3如何读写对象 11.4字符流的输入输出 11.4.1认识字符流 11.4.2如何读写文件 11.4.3如何进行键盘输入 11.5和IO操作相关的其他类 11.5.1用RandomAccessFile类进行文件读写 11.5.2使用Properties类 11.6小结 第12章多线程开发 12.1认识多线程 12.1.1为什么需要多线程 12.1.2继承Thread类开发多线程 12.1.3实现Runnable接口开发多线程 12.1.4两种方法有何区别 12.2控制线程运行 12.2.1为什么要控制线程运行 12.2.2传统方法的安全问题 12.2.3如何控制线程的运行 12.3线程同步安全 12.3.1什么是线程同步 12.3.2一个有问题的案例 12.3.3如何解决 12.3.4小心线程死锁 12.4认识定时器 12.4.1为什么需要定时器 12.4.2如何使用定时器 12.5小结 第13章反射技术 13.1为什么要学习反射 13.1.1引入配置文件 13.1.2配置文件遇到的问题 13.2认识Class类 13.2.1什么是Class类 13.2.2如何获取一个类对应的Class对象 13.2.3如何获取类中的成员信息 13.3通过反射机制访问对象 13.3.1如何实例化对象 13.3.2如何给成员变量赋值 13.3.3如何调用成员函数 13.4何时使用反射 13.5动态异常处理框架 13.5.1框架功能简介 13.5.2重要技术 13.5.3框架代码编写 13.5.4使用该框架 13.6小结 第14章实验指导3 14.1字符频率统计软件 14.1.1软件功能简介 14.1.2重要技术 14.1.3项目结构 14.1.4代码编写 14.1.5思考题 14.2文本翻译软件 14.2.1软件功能简介 14.2.2重要技术 14.2.3项目结构 14.2.4代码编写 14.2.5思考题 14.3用享元模式优化程序性能 14.3.1为什么需要享元模式 14.3.2重要技术 14.3.3代码编写 14.3.4思考题 下篇UML 第15章UML入门 15.1认识UML 15.1.1为什么需要UML 15.1.2UML的来历 15.2用Rational Rose进行UML建模 15.2.1什么是Rational Rose 15.2.2安装Rational Rose 15.2.3如何使用Rational Rose 15.2.4UML图的种类 15.3小结 第16章用例图 16.1认识用例图 16.1.1为什么需要用例图 16.1.2什么是用例图 16.2详解用例图 16.2.1系统边界 16.2.2参与者 16.2.3用例 16.2.4箭头 16.2.5注释 16.2.6用Rational Rose画用例图 16.2.7用例规约 16.3一个案例 16.3.1案例描述 16.3.2画出用例图 16.3.3写出用例描述 16.4小结 第17章类图和对象图 17.1认识类图 17.1.1为什么需要类图 17.1.2什么是类图 17.2详解类图 17.2.1类 17.2.2箭头 17.2.3注释 17.2.4用Rational Rose画类图 17.3对象图 17.4小结 第18章实验指导4 18.1用例图练习 18.1.1软件功能简介 18.1.2识别系统中的参与者和用例 18.1.3画出用例图 18.1.4用例描述 18.2类图练习 18.2.1练习1: 根据代码画出类图 18.2.2练习2: 根据需求构建类图 18.3思考题 第19章顺序图、协作图、状态图和活动图 19.1顺序图 19.1.1什么是顺序图 19.1.2详解顺序图 19.1.3用Rational Rose画顺序图 19.2协作图 19.2.1什么是协作图 19.2.2详解协作图 19.2.3用Rational Rose画协作图 19.3状态图 19.3.1什么是状态图 19.3.2详解状态图 19.3.3用Rational Rose画状态图 19.4活动图 19.4.1什么是活动图 19.4.2详解活动图 19.4.3用Rational Rose画活动图 19.5小结 第20章包图、构件图和部署图 20.1包图 20.1.1什么是包图 20.1.2详解包图 20.1.3用Rational Rose画包图 20.2构件图 20.2.1什么是构件图 20.2.2详解构件图 20.2.3用Rational Rose画构件图 20.3部署图 20.3.1什么是部署图 20.3.2详解部署图 20.3.3用Rational Rose画部署图 20.4小结 第21章实验指导5 21.1顺序图、协作图练习 21.1.1功能简介 21.1.2创建顺序图 21.1.3创建协作图 21.2状态图、活动图练习 21.2.1功能简介 21.2.2创建状态图 21.2.3创建活动图 21.3包图、构件图和部署图练习

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值