前言:
如今我们对“java 访问限制由于对必须的库”可能比较看重,看官们都需要了解一些“java 访问限制由于对必须的库”的相关资讯。那么小编在网上网罗了一些有关“java 访问限制由于对必须的库””的相关内容,希望你们能喜欢,小伙伴们一起来了解一下吧!JAVA基础
JAVA中的几种基本数据类型是什么,各自占用多少字节?
2.String类能被继承吗,为什么?
答案是不能的。因为我们查看String源码就知道,String被关键字final修饰,所以不能被继承。
3.在自己的代码中,如果创建一个java.lang.String类,这个类是否可以被类加载器加载?为什么?
说到类加载,就必须对类加载机制非常熟悉。
类加载使用的是双亲委派模型,当你想要加载一个类的时候,必须先给你的父加载器,它再去想办法加载,如果它不能加载,再告诉我们,我们自己想办法。
所以,在java中java.lang.String肯定在上层的ClassLoader被加载过了,所以你自己写的完全没有机会加载。
4.String,Stringbuffer,StringBuilder的区别。
5.ArrayList和LinkedList有什么区别。
1、数据结构不同
ArrayList是Array(动态数组)的数据结构,LinkedList是Link(链表)的数据结构。
2、效率不同
当随机访问List(get和set操作)时,ArrayList比LinkedList的效率更高,因为LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找。
当对数据进行增加和删除的操作(add和remove操作)时,LinkedList比ArrayList的效率更高,因为ArrayList是数组,所以在其中进行增删操作时,会对操作点之后所有数据的下标索引造成影响,需要进行数据的移动。
3、自由性不同
ArrayList自由性较低,因为它需要手动的设置固定大小的容量,但是它的使用比较方便,只需要创建,然后添加数据,通过调用下标进行使用;而LinkedList自由性较高,能够动态的随数据量的变化而变化,但是它不便于使用。
4、主要控件开销不同
ArrayList主要控件开销在于需要在lList列表预留一定空间;而LinkList主要控件开销在于需要存储结点信息以及结点指针信息。
6.什么是序列化,怎么序列化,为什么序列化,反序列化会遇到什么问题,如何解决?
什么是序列化
Java对象序列化是指将Java对象转化为·字节序列的过程而反序列化则是将字节序列转化为Java对象的过程
为什么需要序列化
我们知道不同线程/进程进行远程通信时可以相互发送各种数据,包括文本图片音视频等,Java对象不能直接传输,所以需要转化为二进制序列传输,所以需要序列化
怎么序列化
实现Serializable接口实现这个接口就会添加一个serialVersionUID
的版本号,这个版本号的作用是用来验证反序列化时是否用的是同一个类
序列化是通过ObjectOutputStream类的writeObject()方法将对象直接写出
反序列化时通过ObjectInputStream类的readObject()方法从流中读取数据
反序列化会遇到什么问题
可能会导致InvalidClassException异常
如果没有显式声明序列版本UID,对对象的需求进行了改动,那么兼容性将会遭到破坏,在运行时导致InvalidClassException
7.讲讲类的实例化顺序,比如父类静态数据,构造函数,字段,子类静态数据,构造函数,字段,当new的时候,他们的执行顺序。
父类静态变量、
父类静态代码块、
子类静态变量、
子类静态代码块、
父类非静态变量(父类实例成员变量)、
父类构造函数、
子类非静态变量(子类实例成员变量)、
子类构造函数。
8.用过哪些Map类,都有什么区别,HashMap是线程安全的吗,并发下使用的Map是什么,他们内部原理分别是什么,比如存储方式,hashcode,扩容,默认容量等。
HashMap、HashTable、LinkedHashMap和TreeMap。
HashMap
HashMap 是一个最常用的Map,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。
遍历时,取得数据的顺序是完全随机的。
HashMap最多只允许一条记录的键为Null;允许多条记录的值为 Null
HashMap不支持线程的同步,是非线程安全的,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要同步,可以用 Collections和synchronizedMap方法使HashMap具有同步能力,或者使用ConcurrentHashMap。
Hashtable
Hashtable与 HashMap类似,它继承自Dictionary类,不同的是:
它不允许记录的键或者值为空。
它支持线程的同步,即任一时刻只有一个线程能写Hashtable,因此也导致了 Hashtable在写入时会比较慢。
LinkedHashMap
LinkedHashMap 保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的。在遍历的时候会比HashMap慢,不过有种情况例外,当HashMap容量很大,实际数据较少时,遍历起来可能会比LinkedHashMap慢,因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和容量有关。
TreeMap
TreeMap实现SortMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator 遍历TreeMap时,得到的记录是排过序的。
区别
一般情况下,我们用的最多的是HashMap,HashMap里面存入的键值对在取出的时候是随机的,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。在Map中插入、删除和定位元素,HashMap 是最好的选择。
TreeMap取出来的是排序后的键值对。但如果要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。
LinkedHashMap是HashMap的一个子类,如果需要输出的顺序和输入的相同,那么用LinkedHashMap可以实现,它还可以按读取顺序来排列,像连接池中可以应用。
9.JAVA8的ConcurrentHashMap为什么放弃了分段锁,有什么问题吗,如果你来设计,你如何设计。
jdk8 放弃了分段锁而是用了Node锁,减低锁的粒度,提高性能,并使用CAS操作来确保Node的一些操作的原子性,取代了锁。
但是ConcurrentHashMap的一些操作使用了synchronized锁,而不是ReentrantLock,虽然说jdk8的synchronized的性能进行了优化,但是我觉得还是使用ReentrantLock锁能更多的提高性能
10.有没有有顺序的Map实现类,如果有,他们是怎么保证有序的。
Hashmap和Hashtable 都不是有序的。
TreeMap和LinkedHashmap都是有序的。(TreeMap默认是key升序,LinkedHashmap默认是数据插入顺序)
TreeMap是基于比较器Comparator来实现有序的。
LinkedHashmap是基于链表来实现数据插入有序的。
11.数组和链表数据结构描述,各自的时间复杂度。
一、各自的特点:
数组:
数组是将元素在内存中连续存放,由于每个元素占用内存相同,可以通过下标迅速访问数组中任何元素。但是如果要在数组中增加一个元素,需要移动大量元素,在内存中空出一个元素的空间,然后将要增加的元素放在其中。同样的道理,如果想删除一个元素,同样需要移动大量元素去填掉被移动的元素。如果应用需要快速访问数据,很少或不插入和删除元素,就应该用数组。
链表:
链表恰好相反,链表中的元素在内存中不是顺序存储的,而是通过存在元素中的指针联系到一起。比如:上一个元素有个指针指到下一个元素,以此类推,直到最后一个元素。如果要访问链表中一个元素,需要从第一个元素开始,一直找到需要的元素位置。但是增加和删除一个元素对于链表数据结构就非常简单了,只要修改元素中的指针就可以了。如果应用需要经常插入和删除元素你就需要用链表数据结构了。
二、数组和链表的区别:
1、从逻辑结构角度来看:
数组必须事先定义固定的长度(元素个数),不能适应数据动态地增减的情况。当数据增加时,可能超出原先定义的元素个数;当数据减少时,造成内存浪费。
链表动态地进行存储分配,可以适应数据动态地增减的情况,且可以方便地插入、删除数据项。(数组中插入、删除数据项时,需要移动其它数据项)
2、数组元素在栈区,链表元素在堆区;
3、从内存存储角度来看:
(静态)数组从栈中分配空间, 对于程序员方便快速,但自由度小。
链表从堆中分配空间, 自由度大但申请管理比较麻烦。
数组利用下标定位,时间复杂度为O(1),链表定位元素时间复杂度O(n);
数组插入或删除元素的时间复杂度O(n),链表的时间复杂度O(1)。
12.Java中的HashSet内部是如何工作的。
HashSet 的内部采用 HashMap来实现。由于 Map 需要 key 和 value,所以HashSet中所有 key 的都有一个默认 value。
类似于HashMap,HashSet 不允许重复的 key,只允许有一个null key,意思就是 HashSet 中只允许存储一个 null 对象。
如果需要详细了解,还是看一下HashSet的源码比较好。
13.抽象类和接口的区别,类可以继承多个类么,接口可以继承多个接口么,类可以实现多个接口么。
1、接口可以继承接口,抽象类不可以继承接口,但可以实现接口。
2、抽象类可以继承实体类。抽象类可以实现(implements)接口,抽象类是否可继承实体类,但前提是实体类必须有明确的构造函数。
3.抽象类可以继承实体类,就是因为抽象类的可以继承性和有方法。
4、一个接口可以继承多个接口. interface C extends A, B {}是可以的. 一个类可以实现多个接口: class D implements A,B,C{} 但是一个类只能继承一个类,不能继承多个类 class B extends A{} 在继承类的同时,也可以继承接口: class E extends D implements A,B,C{} 这也正是选择用接口而不是抽象类的原因。
14.继承和聚合的区别在哪。
继承指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系;在Java中此类关系通过关键字extends明确标识,在设计时一般没有争议性。
聚合是关联关系的一种特例,他体现的是整体与部分、拥有的关系,即has-a的关系,此时整体与部分之间是可分离的,他们可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享;比如计算机与CPU、公司与员工的关系等;表现在代码层面,和关联关系是一致的,只能从语义级别来区分。
15.final的用途。
final: 修饰符,可以用于修饰类、成员方法和成员变量
final所修饰的类:不能被继承,不能有子类
final所修饰的方法:不能被重写
final所修饰的变量:是不可以修改的,是常量
注意:final所修饰的变量必须初始化,可以选择直接赋值初始化或者用构造方法初始化
16.请结合OO设计理念,谈谈访问修饰符public、private、protected、default在应用设计中的作用。
访问修饰符,主要标示修饰块的作用域,方便隔离防护。
public: Java语言中访问限制最宽的修饰符,一般称之为“公共的”。被其修饰的类、属性以及方法不仅可以跨类访问,而且允许跨包(package)访问。
private: Java语言中对访问权限限制的最窄的修饰符,一般称之为“私有的”。被其修饰的类、属性以及方法只能被该类的对象访问,其子类不能访问,更不能允许跨包访问。
protect: 介于public 和 private 之间的一种访问修饰符,一般称之为“保护形”。被其修饰的类、属性以及方法只能被类本身的方法及子类访问,即使子类在不同的包中也可以访问。
default:即不加任何访问修饰符,通常称为“默认访问模式“。该模式下,只允许在同一个包中进行访问。
17.深拷贝和浅拷贝区别。
深拷贝和浅拷贝最根本的区别在于是否真正获取一个对象的复制实体,而不是引用。
假设B复制了A,修改A的时候,看B是否发生变化:
如果B跟着也变了,说明是浅拷贝,拿人手短!(修改堆内存中的同一个值)
如果B没有改变,说明是深拷贝,自食其力!(修改堆内存中的不同的值)
18.说一说你对java.lang.Object对象中hashCode和equals方法的理解。在什么场景下需要重新实现这两个方法。
1)对于equals()与hashcode(),比较通用的规则:
①两个obj,如果equals()相等,hashCode()一定相等
②两个obj,如果hashCode()相等,equals()不一定相等
2)在设计一个类的时候往往需要重写equals方法,比如String类,在重写equals方法的同时,必须重写hashCode方法。
如果我们对一个对象重写了euqals,意思是只要对象的成员变量值都相等那么euqals就等于true,但不重写hashcode,那么我们再new一个新的对象,当原对象.equals(新对象)等于true时,两者的hashcode却是不一样的,由此将产生了理解的不一致,如在存储散列集合时(如Set类),将会存储了两个值一样的对象,导致混淆,因此,就也需要重写hashcode()。
通俗一点就是在例如hashMap集合中put一个对象的时候其实就是将一个对象的值(属性、哈希值)放入,如果在根据这个对象get取出来时也会考虑它的哈希值,但是get时如果没有重写hashcode()方法,你的新对象就是一个新的哈希值与原对象不想等,应该get时肯定为空。
19.如何在父类中为子类自动完成所有的hashcode和equals实现?这么做有何优劣。
hashCode与equals方法都是Java Object对象中的方法
也就是说Java的一切对象都提供这两个方法。
这两个方法在Java中有着不一般的联系;
在Java类操作中,也起着至关重要的计算依据。
当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。
如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;
如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址。
所以这里存在一个冲突解决的问题。
好处是:我们不用自己去写。
缺点是:有时候父类中equals和hashcode方法不满足我们的需求,需要重写。
hashCode是所有java对象的固有方法
如果不重载的话,返回的实际上是该对象在jvm的堆上的内存地址,而不同对象的内存地址肯定不同,所以这个hashCode也就肯定不同了。
如果重载了的话,由于采用的算法的问题,有可能导致两个不同对象的hashCode相同。
java中的hash函数返回的是int类型的
也就是说,最多允许存在2^32个分组,也是有限的,所以出现相同的哈希码就不稀奇了
20.有没有可能2个不相等的对象有相同的hashcode。
在产生hash冲突时,两个不相等的对象就会有相同的 hashcode 值.当hash冲突产生时,一般有以下几种方式来处理:
PhantomReference prf = new PhantomReference(new
String(“str”), new ReferenceQueue<>());
List iniData = new ArrayList<>()
21.error和exception的区别,CheckedException,RuntimeException的区别。
接下来Throwable分成了两个不同的分支,一个分支是Error,它表示不希望被程序捕获或者是程序无法处理的错误。另一个分支是Exception,它表示用户程序可能捕捉的异常情况或者说是程序可以处理的异常。其中异常类Exception又分为运行时异常(RuntimeException)和非运行时异常。
Java异常又可以分为不受检查异常(Unchecked Exception)和检查异常(Checked Exception)。
下面将详细讲述这些异常之间的区别与联系:
Error:Error类对象由 Java 虚拟机生成并抛出,大多数错误与代码编写者所执行的操作无关。例如,Java虚拟机运行错误(Virtual MachineError),当JVM不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止;还有发生在虚拟机试图执行应用时,如类定义错误(NoClassDefFoundError)、链接错误(LinkageError)。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在Java中,错误通常是使用Error的子类描述。
Exception:在Exception分支中有一个重要的子类RuntimeException(运行时异常),该类型的异常自动为你所编写的程序定义ArrayIndexOutOfBoundsException(数组下标越界)、NullPointerException(空指针异常)、ArithmeticException(算术异常)、MissingResourceException(丢失资源)、ClassNotFoundException(找不到类)等异常,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生;而RuntimeException之外的异常我们统称为非运行时异常,类型上属于Exception类及其子类,从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。
22.请列出5个运行时异常。
AnnotationTypeMismatchException,
ArithmeticException,
ArrayStoreException,
BufferOverflowException,
BufferUnderflowException,
CannotRedoException,
CannotUndoException,
ClassCastException,
CMMException,
23.在jdk1.5中,引入了泛型,泛型的存在是用来解决什么问题。
首先我们要了解:什么是向下转型和向上转型。
面向对象的转型只会发生在具有继承关系的父子类中(接口也是继承的一种)
向上转型:其核心目的在于参数的统一上,根本不需要强制类型转换。
向下转型:是为了操作子类定义的特殊功能,需要强制类型转换,可是现在存在的问题是:向下转型其实是一种非常不安全的操作,以为编译的时候,程序不会报错,而在运行的时候会报错,这就是传说中的—迷之报错。
不过呢,在JDK1.5之后,新增加了泛型的技术,这就将上述向下转型的问题消灭在了萌芽之中。
泛型的核心意义在于:类在进行定义的时候可以使用一个标记,此标记就表示类中属性或者方法以及参数的类型,标记在使用的时候,才会去动态的设置类型。
24.java8的新特性。
Lambda 表达式 − Lambda 允许把函数作为一个方法的参数(函数作为参数传递到方法中)。方法引用 − 方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。默认方法 − 默认方法就是一个在接口里面有了一个实现的方法。新工具 − 新的编译工具,如:Nashorn引擎 jjs、 类依赖分析器jdeps。Stream API −新添加的Stream API(java.util.stream) 把真正的函数式编程风格引入到Java中。Date Time API − 加强对日期与时间的处理。Optional 类 − Optional 类已经成为 Java 8 类库的一部分,用来解决空指针异常。Nashorn, JavaScript 引擎 − Java 8提供了一个新的Nashorn javascript引擎,它允许我们在JVM上运行特定的javascript应用
25.IO模型有哪些,讲讲你理解的nio ,他和bio,aio的区别是啥,谈谈reactor模型。
1.什么是BIO,NIO,AIO
JAVA BIO:同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程并处理,如果这个连接不做任何事情会造成不必要的开销,当然可以通过线程池机制改善
JAVA NIO:同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求时才启动一个线程进行处理
JAVA AIO(NIO2):异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理
2.使用场景
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
3.BIO 同步并阻塞
tomcat采用的传统的BIO(同步阻塞IO模型)+线程池模式,对于十万甚至百万连接的时候,传统BIO模型是无能为力的:
①线程的创建和销毁成本很高,在linux中,线程本质就是一个进程,创建销毁都是重量级的系统函数
②线程本身占用较大的内存,像java的线程栈一般至少分配512K-1M的空间,如果系统线程过高,内存占用是个问题
③线程的切换成本高,操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用,如果线程数过高可能执行线程切换的时间甚至大于线程执行的时间,这时候带来的表现是系统load偏高,CPUsy使用率很高
④容易造成锯齿状的系统负载。系统负载是用活动线程数或CPU核心数,一旦线程数量高但外部网络环境不是很稳定,就很容易造成大量请求的结果同时返回,激活大量阻塞线程从而使系统负载压力过大。
4NIO同步非阻塞
NIO基于Reactor,当socket有流可读或可写入socket,操作系统会相应的通知引用程序进行处理,应用再将流读取到缓冲区或写入操作系统。也就是,不是一个链接就要对应一个处理线程,而是一个有效请求对应一个线程,当连接没有数据时,是没有工作线程来处理的
Reactor模型
26.写出三种单例模式实现 。
单例模式:单例模式的意思就是只有一个实例。单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。单例模式有三种:懒汉式单例,饿汉式单例,登记式单例。1.懒汉式单例public class Singleton { private static Singleton singleton; private Singleton() {} //此类不能被实例化 public static synchronized Singleton getInstance() { if (singleton == null) { singleton = new Singleton(); } return singleton; }}优点:第一次调用才初始化,避免内存浪费。缺点:必须加锁synchronized 才能保证单例,(如果两个线程同时调用getInstance方法,会chuxia)但加锁会影响效率。2.饿汉式单例public class Singleton { private static final Singleton SINGLETON = new Singleton(); private Singleton() {} //此类不能被实例化 public static Singleton getInstance() { return SINGLETON; }}优点:没有加锁,执行效率会提高。缺点:类加载时就初始化,浪费内存。3.登记式模式(holder)public class Singleton { private Singleton() {} //构造方法是私有的,从而避免外界利用构造方法直接创建任意多实例。 public static Singleton getInstance() { return Holder.SINGLETON; } private static class Holder { private static final Singleton SINGLETON = new Singleton(); }}内部类只有在外部类被调用才加载,产生SINGLETON实例;又不用加锁。此模式有上述两个模式的优点,屏蔽了它们的缺点,是最好的单例模式。
27.反射的原理,反射创建类实例的三种方式是什么。反射中,Class.forName和ClassLoader区别 。
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性。这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
反射原理(Class对象获取及实例创建的三种方法)
要想通过反射获取一个类的信息,首先要获取该类对应的Class类实例,Class类的实例代表了正在运行中的Java应用的类和接口。Class类没有公共的构造方法,Class类对象是在二进制字节流(一般是.class文件,也可通过网络或zip包等路径获取)被JVM加载时,通过调用类加载器的defineClass()方法来构建的。(译自Class类的JDK源码)
// 1Class<TestClass> class = Class.forName("com.zkw.TestClass");// 2Clas<TestClass> class = TestClass.class;// 3TestClass tc = new TestClass();Class<TestClass> class = tc.getClass(); // 通过Class对象创建类实例TestClass testClass = class.newInstance();
28.描述动态代理的几种实现方式,分别说出相应的优缺点。
AOP的源码中用到了两种动态代理来实现拦截切入功能:jdk动态代理和cglib动态代理。两种方法同时存在,各有优劣。
jdk动态代理是由java内部的反射机制来实现的,cglib动态代理底层则是借助asm来实现的。
29.动态代理与cglib实现的区别。
jdk动态代理是jdk原生就支持的一种代理方式,它的实现原理,就是通过让target类和代理类实现同一接口,代理类持有target对象,来达到方法拦截的作用,这样通过接口的方式有两个弊端,一个是必须保证target类有接口,第二个是如果想要对target类的方法进行代理拦截,那么就要保证这些方法都要在接口中声明,实现上略微有点限制。
Cglib是一个优秀的动态代理框架,它的底层使用ASM在内存中动态的生成为代理类的子类,使用CGLIB即使代理类没有实现任何接口也可以实现动态代理功能。CGLIB具有简单易用,它的运行速度要远远快于JDK的Proxy动态代理:
cglib有两种可选方式,继承和引用。第一种是基于继承实现的动态代理,所以可以直接通过super调用target方法,但是这种方式在spring中是不支持的,因为这样的话,这个target对象就不能被spring所管理,所以cglib还是采用类似jdk的方式,通过持有target对象来达到拦截方法的效果。
30.为什么CGlib方式可以对接口实现代理。
对接口进行代理的cglib,最后生成的源码是实现了该接口和Factory接口
对实现类进行代理的cglib,最后生成的源码是继承了实现类并实现了Factory接口
标签: #java 访问限制由于对必须的库