1、注解
1.1、谈谈你对Java平台的理解? “Java是解释执行”,这句话正确吗?谈谈你对Java平台的理解?(jdk7增加对switch的字符串支持 jdk8增加函数式编程)
序号 | 特点 |
1 | Java本身是一种面向对象的语言,最显著的特性有两个方面,一是跨平台能力(分为编译期和运行期,编译期源码生成字节码,运行期jvm通过类加载器加载字节码,解释或编译执行),二是垃圾收集(GC),Java通过垃圾收集器(Garbage Collector)回收分配内存 |
2 | Java语言特性,包括泛型、Lambda等语言特性;基础类库,包括集合、IO/NIO、网络、并发、安全等基础类库 |
3 | 谈谈JVM的一些基础概念和机制,比如Java的类加载机制,常用版本JDK(如JDK8)内嵌的Class-Loader |
4 | JDK包含哪些工具:编译器、运行时环境、安全工具、诊断和监控工具 |
1.2、注解的概念
注解(Annotation)是Java提供的设置程序中元素的关联信息和元数据(MetaData)的方法,它是一个接口,程序可以通过反射获取指定程序中元素的注解对象,然后通过该注解对象获取注解中的元数据信息。
1.3、标准元注解:@Target、@Retention、@Documented、@Inherited
元注解(Meta-Annotation)负责注解其他注解。在Java中定义了4个标准的元注解类型@Target、@Retention、@Documented、@Inherited,用于定义不同类型的注解。
(1)@Target:@Target说明了注解所修饰的对象范围。注解可被用于packages、types(类、接口、枚举、注解类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(循环变量、catch参数等)。在注解类型的声明中使用了target,可更加明确其修饰的目标,target的具体取值类型如表所示
(2)@Retention:@Retention定义了该注解被保留的级别,即被描述的注解在什么级别有效,有以下3种类型。◎ SOURCE:在源文件中有效,即在源文件中被保留。◎ CLASS:在Class文件中有效,即在Class文件中被保留。◎ RUNTIME:在运行时有效,即在运行时被保留。(3)@Documented:@Documented表明这个注解应该被javadoc工具记录,因此可以被javadoc类的工具文档化。
(4)@Inherited:@Inherited是一个标记注解,表明某个被标注的类型是被继承的。如果有一个使用了@Inherited修饰的Annotation被用于一个Class,则这个注解将被用于该Class的子类。
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface LogRecord { /** * 参数名称,通过该key获取对应的value * @return string */ String paramName() default ""; /** * 业务类别 {@link BizTypeEnum} */ BizTypeEnum type() default BizTypeEnum.UN_KNOWN; /** * 参数类别,可以知道入参的类型是啥 */ LogRecordParamTypeEnum paramType() default LogRecordParamTypeEnum.UN_KNOWN; /** * 操作类型 可以知道做了什么操作 * @return OperateTypeEnum */ OperateTypeEnum operateType() default OperateTypeEnum.UNKNOWN; }
1.4、注解处理器
注解用于描述元数据的信息,使用的重点在于对注解处理器的定义。Java SE5扩展了反射机制的API,以帮助程序快速构造自定义注解处理器。对注解的使用一般包含定义及使用注解接口,我们一般通过封装统一的注解工具来使用注解。
1、下面的代码定义了一个FruitProvider注解接口,其中有name和address两个属性
2、使用注解接口下面的代码定义了一个Apple类,并通过注解方式定义了一个FruitProvider:
3、定义注解处理器
下面的代码定义了一个FruitInfoUtil注解处理器,并通过反射信息获取注解数据,最后通过main方法调用该注解处理器使用注解
4、Java反射
4.1、谈谈Java反射机制,动态代理是基于什么原理?
背景:动态语言,指在程序运行时可以改变其结构的语言,比如新的属性或方法的添加、删除等结构上的变化。JavaScript、Ruby、Python 等都属于动态语言;C、C++不属于动态语言,从反射的角度来说,Java属于半动态语言。
1、什么是java反射机制?(java.lang.reflect)
反射机制是Java语言提供的一种基础功能,赋予程序在运行时自省的能力。通过反射我们可以直接操作类或者对象,比如获取某个对象的类定义,获取类声明的属性和方法,调用方法或者构造对象,甚至可以运行时修改类定义
4.2、Java中的反射首先能够获取到java中要反射类的字节码,获取字节码的三种方法:
1、class.forName(className)
2、类名.class
3、this.getClass() 然后将字节码中的方法、变量、构造函数等映射成相应的Method、Filed、Constructor等类
注意点:
- 1、反射提供的AccessibleObject.setAccessible?(boolean flag)。它的子类也大都重写了这个方法,这里的所谓accessible可以理解成修饰成员的public、protected、private,这意味着我们可以在运行时修改成员访问限制
- 2、setAccessible的应用场景非常普遍:
在ORM框架中,我们为一个Java实体对象,运行时自动生成setter、getter的逻辑
绕过API访问控制:自定义的高性能NIO框架需要显式地释放DirectBufer,使用反射绕开限制是一种常见办法 - 3、java9中,处于强封装性的考虑,对反射访问做了限制,只有当被反射操作的模块和指定的包对反射调用者模块Open,才能使用setAccessible;
4.3、Java反射机制的常见应用
Java中的对象有两种类型:编译时类型和运行时类型。编译时类型指在声明对象时所采用的类型,运行时类型指为对象赋值时所采用的类型。
如下代码:person对象的编译时类型为Person,运行时类型为Student,因此无法在编译时获取在Student类中定义的方法:
Person person = new Student();
因此,程序在编译期间无法预知该对象和类的真实信息,只能通过运行时信息来发现该对象和类的真实信息,而其真实信息(对象的属性和方法)通常通过反射机制来获取,这便是Java语言中反射机制的核心功能。
应用
- 动态代理(AOP/RPC);
- 提供第三方开发者扩展能力(Servlet容器/JDBC连接);
- 第三方组件创建对象(DI);
- Lambda实现机制
4.4、Java反射API
反射API主要用于在运行过程中动态生成类、接口或对象等信息,其常用API如下:
- Class类:用于获取类的属性、方法等信息;
- Field类:表示类的成员变量,用于获取和设置类中的属性值;
- Method类:表示类的方法,用于获取方法的描述信息或者执行某个方法;
- Constructor类:表示类的构造方法。
反射的步骤如下。
(1)获取想要操作的类的Class对象,该Class对象是反射的核心,通过它可以调用类的任意方法。
(2)调用Class对象所对应的类中定义的方法,这是反射的使用阶段。
(3)使用反射API来获取并调用类的属性和方法等信息。
获取Class对象的3种方法如下。
(1)调用某个对象的getClass方法以获取该类对应的Class对象
Person p = new Person(); Class clazz = Person.getClass();
(2)调用某个类的class属性以获取该类对应的Class对象:
Class clazz = Person.class;
(3)调用Class类中的forName静态方法以获取该类对应的Class对象,这是最安全、性能也最好的方法:
Class clazz = class.forName(fullClassName); //fullClassName为包路径及名称
我们在获得想要操作的类的Class对象后,可以通过Class类中的方法获取并查看该类中的方法和属性,具体代码如下
4.5、创建对象的两种方式
创建对象的两种方式如下。
◎ 使用Class对象的newInstance方法创建该Class对象对应类的实例,这种方法要求该Class对象对应的类有默认的空构造器。
◎ 先使用Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance方法创建Class对象对应类的实例,通过这种方法可以选定构造方法创建实例
创建对象的具体代码如下:
4.6、Method的invoke方法
Method提供了关于类或接口上某个方法及如何访问该方法的信息,那么在运行的代码中如何动态调用该方法呢?答案就通过调用Method的invoke方法。我们通过invoke方法可以实现动态调用,比如可以动态传入参数及将方法参数化。
具体过程为:获取对象的Method,并调用Method的invoke方法,如下所述。
(1)获取Method对象:通过调用Class对象的getMethod(String name, Class<? >…parameterTypes)
返回一个Method对象,它描述了此Class对象所表示的类或接口指定的公共成员方法。name参数是String类型,用于指定所需方法的名称。parameterTypes参数是按声明顺序标识该方法的形参类型的Class对象的一个数组,如果parameterTypes为null,则按空数组处理。
(2)调用invoke方法:指通过调用Method对象的invoke方法来动态执行函数。invoke方法的具体使用代码如下:
以上代码
- 首先通过Class.forName方法获取Person类的Class对象;
- 然后调用Person类的Class对象的getMethod(“setName”, String.class)获取一个method对象;
- 接着使用Class对象获取指定的Constructor对象并调用Constructor对象的newInstance方法创建Class对象对应类的实例;
- 最后通过调用method.invoke方法实现动态调用,这样就通过反射动态生成类的对象并调用其方法
4.3、什么是动态代理?(延伸出来的一种广泛应用于产品开发中的技术)
动态代理是一种方便运行时动态构建代理、动态处理代理方法调用的机制,很多场景都是利用类似机制做到的,比如用来包装RPC调用、面向切面的编程(AOP)
通过代理可以让调用者与实现者之间解耦。比如进行RPC调用,框架内部的寻址、序列化、反序列化等
实现动态代理的方式很多:
- 比如JDK自身提供的动态代理,就是主要利用了上面提到的反射机制。
- 其他的实现方式,比如利用传说中更高性能的字节码操作机制,类似ASM、cglib(基于ASM)、Javassist等
JDK动态代理的例子,下面只是加了一句print,在生产系统中,我们可以轻松扩展类似逻辑进行诊断、限流等
public class MyDynamicProxy { public satic void main (String[] args) { HelloImpl hello = new HelloImpl(); MyInvocationHandler handler = new MyInvocationHandler(hello);//代理接口 // 构造代码实例 Hello proxyHello = (Hello) Proxy.newProxyInsance(HelloImpl.class.getClassLoader(), HelloImpl.class.getInterfaces(), handler); // 调用代理方法 proxyHello.sayHello(); }} interface Hello { void sayHello(); } class HelloImpl implements Hello { @Override public void sayHello() { System.out.println("Hello World"); } } class MyInvocationHandler implements InvocationHandler { private Object target; public MyInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Sysem.out.println("Invoking sayHello"); Object result = method.invoke(target, args); return result; } }/*invorking sayhello hello world*/
- 1、实现对应的 InvocationHandler;
- 2、以接口Hello为纽带,为被调用目标构建代理对象,进而应用程序就可以使用代理对象间接运行调用目标的逻辑,代理为应用插入额外逻辑(这里是println)提供了便利的入口.
- 局限性:它是以接口为中心,相当于添加了一种对于被调用者没有太大意义的限制。实例化的是Proxy对象,而不是真正的被调用类型 //如果选择cglib方式,你会发现对接口的依赖被克服
4.4、写一个ArrayList的动态代理类
final List<String> list = new ArrayList<String>(); List<String> proxyInstance = (List<String>)Proxy.newProxyInstance(list.getClass().getClassLoader(), list.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return method.invoke(list, args); } }); proxyInstance.add("你好");
- 动态代理与静态代理的区别?
1、静态代理:事先写好代理类,可以手工编写,也可以用工具生成。缺点是每个业务类都要对应一个代理类,非常不灵活。
动态代理:运行时自动生成代理对象。缺点是生成代理代理对象和调用代理方法都要额外花费时间
2、动态代理是实现JDK里的 InvocationHandler 接口的invoke方法,但注意的是代理的是接口,也就是你的业务类必须要实现接口,通过Proxy里的 newProxyInstance 得到代理对象,还有一种动态代理CGLIB,代理的是类,不需要业务类继承接口,通过派生的子类来实现代理。通过在运行时,动态修改字节码达到修改类的目的。
AOP编程就是基于动态代理实现的,比如著名的Spring框架、Hibernate框架等等都是动态代理的使用例子。
JDK动态代理:基于Java反射机制实现,必须要实现了接口的业务类才能用这种办法生成代理对象。新版本也开始结合ASM机制。
cglib动态代理:基于ASM机制实现,通过生成业务类的子类作为代理类
4.5、动态代理解决了什么问题,在你业务系统中的应用场景是什么?
JDK动态代理反射慢因为一是获取Field, Method等要进行元数据的查找,这里有字符串匹配操作
dubboFilter
testCase
待补充
4.6、cglib是怎么实现对目标对象的拦截的呢?
采用cglib方式的动态代理还有个缺点:不能应用到被代理对象的final方法上
待补充
5、Java基本语法
5.1、Java的四个基本特性(抽象、封装、继承、多态),对多态的理解(多态的实现方式)以及在项目中的那些地方用到了多态?
- 1、Java面向对象的四个特性
- 封装:为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口 。面向对象的本质就是将现实世界描绘成一系列完全自治、封闭的对象。我们在类中编写的方法就是对实现细节的一种封装;我们编写一个类就是对数据和数据操作的封装。可以说:封装就是隐藏一切可隐藏的东西,只向外界提供最简单的编程接口。
- 抽象:抽象是将一类对象的共同特性 总结出来构造类的过程,包括数据抽象和行为抽象两方面,抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
- 继承:继承是从已有类得到继承信息创建新类的过程。继承让变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因素的重要手段。
- 多态:允许不同子类型的对象对同一消息作出不同的响应。
- 2、对多态的理解(多态的实现方式)
- 多态性分为编译时的多态性和运行时的多态性。
方法重载(overload)实现的是编译时的多态性(仅仅返回值不同,不算方法重载,编译出错)
方法重写(override)实现的是运行时的多态性
实现多态需要做两件事:1、方法重写(子类继承父类并重写父类中已有的或抽象的方法); 2、对象造型(用父类型引用 引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)
- 3、项目中对多态的应用
- 工作台系统中,有两种校验类型,按公务卡校验和按手机号校验。他们有相同的方法CheckInfo(), 但是校验的数据是不同的,一个来源于用户表,一个来源于公务卡系统,也就是校验时会有不同的操作,两种校验操作都继承父类的checkInfo()方法,但对于不同对象,拥有不同的操作。 20210712 刚做的项目。
5.2、面向对象和面向过程的区别?用面向过程可以实现面向对象吗?那是不是不能面向对象?
1、面向对象和面向过程的区别?
- 面向过程就像是一个细心的管家,事无巨细的都要考虑到。而面向对象就像是家用电器,你字需要知道他的功能,不需要知道它的工作原理;
- “面向过程”是一种以事件为中心的编程思想。就是分析出解决问题所需的步骤,然后用函数把这些步骤实现,并按顺序调用。面向对象是以“对象”为中心的编程思想。
- 举个例子:汽车发动、汽车到站
- 这对于“面向过程”来说,是两个事件,汽车启动是一个事件,汽车到站是另一个事件,面向过程编程的过程中我们关心的是事件,而不是汽车本身。针对上述两个事件,形成两个函数,之后依次调用;
- 然而对于面向对象来说,我们关注的是汽车这类对象,两个事件只是这类对象所具有的行为。而且对于这两个行为的顺序没有强制要求。
2、用面向过程可以实现面向对象吗?那是不是不能面向对象?
5.3、重载和重写,如何确定调用哪个函数?
- 重载: 重载发生在同一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者两者都不同)则视为重载;
- 重写: 重写发生在子类和父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写更好访问,不能比父类被重写方法声明更多的异常(里氏替换原则)。根据不同的子类对象确定调用哪个方法。
5.4、接口与抽象类的区别?
相同点:不能被实例化,子类只有实现类接口的方法,或抽象类的方法,才能被实例化
区别: 接口只有定义,方法不能在接口中实现(java8之后,接口也是可以有方法实现的,函数式编程就是只有一个抽象方法的接口),抽象类可以有被实现的方法
接口用implements实现,接口可以多实现 抽象类被extends继承 ,单继承
接口强调对特定功能的实现 常用的功能;抽象类强调所属关系(父子)充当公共类
接口成员方法public 成员变量默认 public static final 赋初值,不能修改 抽象类成员变量default方法(本包可见),抽象方法abstract (不能用private,static,synchronized,native)分号结尾
使用时机:当想要支持多重继承,或是为了定义一种类型请使用接口;当打算提供带有部分实现的“模板”类,而将一些功能需要延迟实现请使用抽象类;当你打算提供完整的具体实现请使用类
5.5、面向对象编程的六个基本设计原则(单一职责、开放封闭、里氏转换、依赖反转、合成聚合复用、接口隔离),迪米特法则,在项目中用过哪些原则 在王争的《设计模式之美》中讲解的很到位
六个基本原则:
单一职责:一个类只做它该做的事情(高内聚)。在面向对象中,如果只让一个类完成它该做的事,而不涉及与它无关的领域就是践行了高内聚的原则,这个类就只有单一职责。
- 类和对象最好只有单一职责,若是承担多种义务,可以考虑进行拆分。
开关原则:软件实体应当对扩展开放,对修改关闭,避免因为新增同类功能而修改已有实现。要做到开闭有两个要点:①抽象是关键,一个系统中如果没有抽象类或接口,系统就没有扩展点; ②封装可变性,将系统中的各种可变因素封装到一个继承结构中,如果多个可变因素混杂在一起,系统将变得复杂而混乱。
里氏替换:进行继承关系抽象时,凡是可以用父类或基类的地方,都可以用子类替换父类型。子类一定是增加父类的能力而不是减少父类的能力。
依赖反转:实体应该依赖抽象而不是实现,面向接口编程(该原则说的直白点具体点就是声明方法的参数类型、方法返回类型、变量的引用类型时,尽可能使用抽象类型而不用具体类型,因为抽象类型可以被它的任何一个子类型所替代)
合成聚合复用:优先使用聚合或合成关系复用代码。
接口分离:不要在一个接口与中定义太多的方法,可以将其拆分成功能单一的多个接口,将行为进行解耦。
迪米特法则
- 迪米特法则又叫最小知识原则,一个对象应当对其他对象有应可能少的了解。
项目中用到的原则:
- 单一职责、开关原则、合成聚合复用(最简单的例子是String类)、接口隔离。
案例:原来的代码:
public class VIPCenter { void serviceVIP(T extend User user>) { if (user insanceof SlumDogVIP) { // 穷X VIP,活动抢的那种 // do somthing } else if(user insanceof RealVIP) { // do somthing } // ... } }
利用开关原则(对拓展开放,对修改关闭),我们可以尝试改造为下面的代码:
interface ServiceProvider{ void service(T extend User user) ; } class SlumDogVIPServiceProvider implements ServiceProvider{ void service(T extend User user){ // do somthing } } class RealVIPServiceProvider implements ServiceProvider{ void service(T extend User user) { // do something } } public class VIPCenter { private Map<User.TYPE, ServiceProvider> providers; void serviceVIP(T extend User user) { providers.get(user.getType()).service(user); } }
5.6 、创建一个类的实例都有哪些办法?
new 工厂模式是对这种方式的包装
clone 克隆一个实例
forclass()然后newInstance() java的反射 反射使用实例:Spring的依赖注入、切面编程中动态代理
实现序列化接口的类,通过IO流反序列化读取一个类,获得实例
- 2、访问权限?
private 当前类
default 同包
protected 子类
public 其他类
5.12、继承和组合的区别和应用场景 20210702补
- 继承和组合的区别
- 优缺点
java开发技巧 | 优点 | 缺点 |
继承 | 1、支持扩展,通过继承父类实现,但会使系统结构较复杂,2、易于修改被复用的代码 | 1、代码白盒复用,父类的实现细节暴露给子类,破坏了封装性;2、当父类的实现代码修改时,可能使得子类也不得不修改,增加维护难度。3、子类缺乏独立性,依赖于父类,耦合度较高 4、不支持动态拓展,在编译期就决定了父类 |
组合 | 1、代码黑盒复用,被包括的对象内部实现细节对外不可见,封装性好。2、整体类与局部类之间松耦合,相互独立。3、支持扩展 4、每个类只专注于一项任务 5、支持动态扩展,可在运行时根据具体对象选择不同类型的组合对象(扩展性比继承好) | 创建整体类对象时,需要创建所有局部类对象。导致系统对象很多。 |
- 结论与使用建议:组合的优点明显多于继承,再加上java中仅支持单继承,所以:
除非两个类之间是is-a的关系,否则尽量使用组合。
5.13、java接口和抽象类的区别,什么时候该用接口什么时候该用抽象类 20210702补
java接口和抽象类的区别,什么时候该用接口什么时候该用抽象类
5.14、Object类中有哪些方法?3个常用,5个线程相关
Object类是Java中其他所有类的祖先,基类
构造方法,
toString(), //toString()方法返回该对象的字符串表示
equals, //比较对象(内存地址)是否相同
hashCode, //返回一个整形数值,表示该对象的哈希值
getClass, //final方法,获得运行时类(class对象)
finalize, //当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法
clone, //实现对象的浅复制,需实现了Cloneable接口;(可重写实现字段深复制)
三个wait(), //调用此方法所在的当前线程等待,直到在其他线程上调用对象的notify(),notifyAll()方法
wait(long timeout) //线程等待,直到notify() notifyAll() 方法,或超过指定的时间量
wait(long timeout, int nanos)//线程等待,notify() notifyAll() 方法,或其他某个线程中断当前线程,或超过时间量
notify, //唤醒在此对象监视器上等待的单个(所有)线程
notifyAll. /方法调用后,线程不会立即释放所持有的锁,直到其所在同步代码块中的代码执行完毕,再释放锁
5.15、你知道Java的继承机制吗?为什么这么做?
单继承多实现
第一方面:
如果一个类继承了类A和类B,A和B都有一个C方法,那么当我们用这个子类对象调用C方法的时候,
jvm就晕了,因为他不能确定你到底是调用A类的C方法还是调用了B类的C方法。
假设A和B都是接口,都有C方法,那么问题就能解决了,因为接口里的方法仅仅是个方法的声明,
并没有实现,子类实现了A和B接口只需要实现一个C方法就OK
第二方面:
Java是严格的面向对象思想的语言,一个孩子只能有一个亲爸爸
5.16、为什么函数不能根据返回类型来区分重载?华为面试
因为调用时不能指定类型信息,编译器不知道你要调用哪个函数
1.float max(int a, int b);
2.int max(int a, int b);
当调用max(1, 2);时无法确定调用的是哪个,单从这一点上来说,仅返回值类型不同的重载是不应该允许的。
5.17、char型变量中能不能存储一个中文汉字,为什么?
char类型可以存储一个中文汉字,因为Java中使用的编码是Unicode,一个char类型占 2 个字节(16 比特),所以放一个中文是没问题的
补充:使用Unicode意味着字符在JVM内部和外部有不同的表现形式,在JVM内部都是Unicode,当这个字符被从JVM内部转移到外部时(例如存入文件系统中),需要进行编码转换。所以 Java 中有字节流和字符流,以及在字符流和字节流之间进行转换的转换流,如InputStreamReader和OutputStreamReader,这两个类是字节流和字符流之间的适配器类,承担了编码转换的任务;
5.18、常用API?
1、Math.round(11.5)等于多少? 12 Math.round(- 11.5) 又等于多少? -11
2、switch 是否能作用在byte上,是否能作用在 long 上,是否能作用在 String上?
long不行,其他都可以(string在java7开始,可以)
3、什么是Java Timer类?如何创建一个有特定时间间隔的任务?
Timer是一个调度器,可以用于安排一个任务在未来的某个特定时间执行或周期性执行,
TimerTask是一个实现了Runnable接口的抽象类,我们需要去继承这个类来创建我们自己的定时任务并使用Timer去安排它的执行
Timer timer = new Timer(); timer.schedule(new TimerTask() { public void run() { System.out.println("abc"); } }, 200000 , 1000);
5.19、请说出下面程序的输出?
class StringEqualTest { public static void main(String[] args) { String s1 = "Programming"; String s2 = new String("Programming"); String s3 = "Program"; String s4 = "ming"; String s5 = "Program" + "ming"; String s6 = s3 + s4; System.out.println(s1 == s2); //false System.out.println(s1 == s5); //true System.out.println(s1 == s6); //false System.out.println(s1 == s6.intern()); //true System.out.println(s2 == s2.intern()); //false }
两个知识点:
- 1.String对象的intern()方法会得到字符串对象在常量池中对应的版本的引用;如果常量池中没有对应的字符串,则该字符串将被添加到常量池中,然后返回常量池中字符串的引用;但是,通过new方法创建的String对象是不检查字符串池的,而是直接在堆区或栈区创建一个新的对象,也不会把对象放入池中
例子:String s = new String(“abc”);创建了几个 String Object?
2个 abc pool中常量池中 new String(“abc”)堆中
student s = new student() 在内存中做了哪些事?
加载student.class文件进内存
在栈内存为s开辟空间;
在堆内存为new student()开辟空间;
对学生对象的成员变量进行默认初始化;
对学生对象的成员变量进行默认初始化;
通过构造方法对学生对象的成员变量赋值;
学生对象初始化完毕,把对象地址赋值给s变量。
- 2.字符串的+操作其本质是创建了StringBuilder 对象进行 append 操作,然后将拼接后的 StringBuilder 对象用 toString 方法处理成 String 对象,
这一点可以用 javap -c StringEqualTest.class 命令获得 class 文件对应的 JVM 字节码指令就可以看出来。
5.20、int和Integer有什么区别?谈谈Integer的值缓存范围***(提醒:越是貌似简单的面试题其中的玄机就越多,需要面试者有相当深厚的功力。)
- 1、理解自动装箱和拆箱?
Java平台为我们自动进行了一些转换,保证不同的写法在运行时等价,它们发生在编译阶段,也就是生成的字节码是一致的
装箱:javac替我们自动把整数装箱转换为Integer.valueOf()
拆箱:拆箱替换为Integer.intValue() - 2、(自动装箱和拆箱 java5引入)下面Integer类型的数值比较输出的结果为?
integer f1=100,f2=100,f3=150,f4=150;
新增静态工厂方法valueOf,在调用它的时候会利用一个缓存机制,如果整型字面量的值在-128到127之间,那么不会new新的Integer对象,而是直接引用常量池
中的Integer对象,所以上面的面试题中f1f2的结果是true,而f3f4的结果是false
bealean;缓存true/false对应的实例,只返回常量实例Boolean.TRUE/FALSE
short:缓存-128/127之间的数值
byte:全部缓存
character:缓存范围‘\u0000’到‘\007F’
以上包装类型都被声明为private final //都是不可变类型 - 3、为什么我们需要原始数据类型,Java的对象似乎也很高效,应用中具体会产生哪些差异?
使用:建议避免无意中的装箱、拆箱行为
使用原始数据类型、数组甚至本地代码实现/替换掉包装类、动态数组,在性能极度敏感的场景往往具有比较大的优势
下面是一个常见的线程安全计数器实现
class Counter { private fnal AtomicLong counter = new AtomicLong(); public void increase() { counter.incrementAndGet(); } }
如果利用原始数据类型,可以将其修改为
class CompactCounter { private volatile long counter; private satic fnal AtomicLongFieldUpdater<CompactCounter> updater = AtomicLongFieldUpdater.newUpdater(CompactCounter.class, "counter"); public void increase() { updater.incrementAndGet(this); } }
- 4、阅读过Integer源码吗?分析下类或某些方法的设计要点。
1、继续深挖缓存,Integer的缓存范围虽然默认是-128到127,但是在特别的应用场景,比如我们明确知道应用会频繁使用更大的数值,这时候应该怎么办呢?
缓存上限值实际是可以根据需要调整的,JVM提供了参数设置:-XX:AutoBoxCacheMax=N
String integerCacheHighPropValue = VM.getSavedProperty(“java.lang.Integer.IntegerCache.high”);
2、引用类型局限性
我们知道Java的对象都是引用类型,如果是一个原始数据类型数组,它在内存里是一段连续的内存,而对象数组则不然,数据存储的是引用,对象往往是分散地存储在堆的不同位
置。这种设计虽然带来了极大灵活性,但是也导致了数据操作的低效,尤其是无法充分利用现代CPU缓存机制 - 5、java中的integerCache
Java5为了减少大量创建Integer的开销、提高性能,采用享元模式,引入Integer实例缓存,将-128至127范围数值的Integer实例缓存在内存中,
这里的缓存就是使用Integer中的辅助性的私有IntegerCache静态类实现。
不仅是Integer,Short、Byte、Long、Char都具有相应的Cache。但是Byte、Short、Long的Cache有固定范围:-128至127;Char的Cache:0至127
这里容易出选择题 - int和Integer的区别?20181114 选择题常考
1、Integer是int的包装类,int是基本类型
2、Integer变量必须实例化后才能使用,而int变量不需要
3、Integer实际是对象的引用,当new一个Integer时,实际上生成一个指针指向此对象;而int则是直接存储数据值
4、Integer的默认值是null,int默认值是0;
延伸:
1、两个通过new生成的Integer变量永远是不相等的
Integer i = new Integer(100); Integer j = new Integer(100); System.out.print(i == j); //false
2、Integer变量和int变量比较时,只要两个变量的值是相等的,则结果为true
//因为包装类integer和基本数据类型int比较时,java会自动拆包为int,然后进行比较
Integer i = new Integer(100); int j = 100; System.out.print(i == j); //true
3、非new生成的Integer变量和new Integer()生成的变量比较时,结果为false
//因为非new生成的Integer变量指向的是java常量池中的对象;new Integer()生成的变量指向堆中新建的对象
Integer i = new Integer(100); Integer j = 100; System.out.print(i == j); //false
4、对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为false
Integer i = 100; Integer i = 128; Integer j = 100; Integer j = 128; System.out.print(i == j); //true System.out.print(i == j); //false
原因:java在编译integer i= 100时,会翻译成integer i = Integer.valueof(100); 而javaAPI对integer类型的valueof定义如下:
java对于-128到127之间的数,会进行缓存,Integer i=127时,会将127进行缓存,下次再写Integer j=127时,就会直接从缓存中取,就不会new了。
6、你知道对象的内存结构是什么样的吗?比如,对象头的结构。如何计算或者获取某个Java对象的大小?
来自深入理解jvm
象由三部分组成,对象头,对象实例,对齐填充
5.21、如何实现对象克隆?
有两种方式。
1、实现Cloneable接口并重写 Object 类中的 clone()方法;
2、实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆。
基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型限定,可以检查出要克隆的对
象是否支持序列化,这项检查是编译器完成的,不是在运行时抛出异常,这种是方案明显优于使用Object 类的 clone
方法克隆对象。让问题在编译的时候暴露出来总是好过把问题留到运行时
5.22、String/StringBuffer/StringBuilder的区别,扩展再问他们的实现?
1、String/StringBuffer/StringBuilder的区别
String 不可变类 值不能被修改 初始化时,能用构造函数,也能赋值
字符串操作不当可能会产生大量临时字符串
String的特性
1、不可变:是指String对象一旦生成,则不能再对它进行改变。不可变的主要作用在于当一个对象需要被多线程共享,并且访问频繁时,可以省略同步和锁等待的时间,从而大幅度提高系统
性能。不可变模式是一个可以提高多线程程序的性能,降低多线程程序复杂度的设计模式
2、针对常量池的优化。当2个String对象拥有相同的值时,他们只引用常量池中的同一个拷贝
string应用场景
字符串内容不经常发生变化的业务场景:常量声明、少量的字符串拼接操作
StringBuffer 可变类 可改变值 只能用构造函数 线程安全(把各种修改数据的方法都加上了synchronized关键字)
我们可以用append或add方法,把字符串添加到已有序列的末尾或指定位置
StringBuffer应用场景:频繁进行字符串的运算(如拼接、替换、删除等),并且运行在多线程环境下,建议使用StringBufer,例如XML解析,HTTP参数解析与封装
StringBuilder(推荐) 可被修改的字符串 线程不安全
为了实现修改字符序列的目的,StringBufer和StringBuilder底层都是利用可修改的(char,JDK 9以后是byte)数组,二者都继承了AbstractStringBuilder,里面包含了基本
操作,区别仅在于最终的方法是否加了synchronized
StringBuilder应用场景:频繁进行字符串的运算(如拼接、替换、删除等),并且运行在单线程环境下,建议使用StringBuilder,例如SQL语句拼装、JSON封装等
2、扩容操作细节:
构建时初始字符串长度加16(如果没有构建对象时输入最初的字符串,那么初始值就是16)
扩容会产生多重开销,因为要抛弃原有数组,创建新的(可以简单认为是倍数)数组,还要进行arraycopy
3、字符串拼接的例子:String myStr = “aa” + “bb” + “cc” + “dd”;?(华为)
在JDK8中,字符串拼接操作会自动被javac转换为StringBuilder操作
JDK9里面则是因为Java9为了更加统一字符串操作优化,提供了StringConcatFactory,作为一个统一的入口
什么情况下用“+”运算符进行字符串连接比调用 StringBuffer/StringBuilder 对象的append方法连接字符串性能更好? 华为
String s = "abc"; String ss = "ok" + s + "xyz" + 5;
反编译
String ss = (new StringBuilder("ok")).append(s).append("xyz").append(5).toString();
在Java中无论使用何种方式进行字符串连接,实际上都使用的是 StringBuilder。
在for循环中,尽量使用stringbuilder不使用“+”
4、字符串缓存(字符串常量池)
方案1:(-XX:+PrintStringTableStatisics)
String在Java 6以后提供了intern()方法,目的是提示JVM把相应字符串缓存起来,以备重复使用。在我们创建字符串对象并调用intern()方法的时候,如果已经有缓存的字符串,
就会返回缓存里的实例,否则将其缓存起来。(由于jdk6被缓存的字符串存在PermGen里,空间有限,使用不当就会OOM,后续版本中,缓存被放置到堆中,JDK8中永久代被MetaSpace元数据区替代)
方案2:(默认关闭 -XX:+UseStringDeduplication)
Oracle JDK 8u20之后,推出了一个新的特性,也就是G1 GC下的字符串排重。它是通过将相同数据的字符串指向同一份数据来做到的,是JVM底层的改变,并不需要Java类库做什么修改
5、String自身的演化
Java的字符串,在历史版本中,它是使用char数组来存数据的,这样非常直接。但是Java中的char是两个bytes大小,拉丁语系语言的字符,根本就不需要太宽的char,这样无区别的实现就造成了一定的浪费
在Java9中,我们引入了Compact Strings的设计,对字符串进行了大刀阔斧的改进,数据存储方式从char数组,改变为一个byte数组加上一个标识编码的所谓coder
5.23、如何理解clone对象 深拷贝和浅拷贝
返回一个object对象的复制。复制函数返回的是新的对象而不是一个引用。
有一个对象 A,在某一时刻 A 中已经包含了一些有效值,此时可能会需要一个和A完全相同新对象B,并且此后对B 任何改动都不会影响到A中的值。
A与B是两个独立的对象,但B的初始值是由A对象确定的。
new 一个对象的过程和 clone 一个对象的过程区别
new操作符的本意是分配内存。程序执行到new操作符时,首先去看new操作符后面的类型,因为知道了类型,才能知道要分配多大的内存空间
分配完内存之后,再调用构造函数,填充对象的各个域,这一步叫做对象的初始化,构造方法返回后,一个对象创建完毕,可以把他的引用
(地址)发布到外部,在外部就可以使用这个引用操纵这个对象。
clone在第一步是和new相似的,都是分配内存,调用clone方法时,分配的内存和原对象(即调用clone方法的对象)相同,
然后再使用原对象中对应的各个域,填充新对象的域,填充完成之后,clone方法返回,一个新的相同的对象被创建,同样可以把这个新对象的引用发布到外部
深拷贝和浅拷贝的原理如下图所示:
Person 中有两个成员变量,分别是name和 age, name 是String类型, age 是 int 类型
age是基本数据类型:直接将一个4字节的整数值拷贝过来就行
name是String类型的,它只是一个引用,指向一个真正的String对象
浅拷贝:stu = (Student)super.clone();
1、创建一个新对象,然后将当前对象的非静态字段(变量)复制该新对象
2、如果字段是值类型的,那么对该字段执行复制;
3、如果该字段是引用类型的话,则复制引用但不复制引用的对象。原始对象及其副本引用同一个对象;改变引用则一起改变
深拷贝:stu.addr = (Address)addr.clone();
1、copy对象所有的内部元素【对象、数组】,最后只剩下原始的类型(int)以及“不可变对象(String)
2、将对象序列化再读出来也是深拷贝
根据原Person对象中的name指向的字符串对象创建一个新的相同的字符串对象,将这个新字符串对象的引用赋给新拷贝的Person对象的name字段
如果想要深拷贝一个对象,这个对象必须要实现Cloneable接口,实现clone方法,并且在 clone 方法内部,把该对象引用的其他对象也要 clone 一份,
这就要求这个被引用的对象必须也要实现Cloneable接口并且实现clone方法。
5.24、==和equals的区别?
==比较的是两个基本变量的值是否相等,引用类型的地址
equals obj默认也是比较对象引用地址,重写后,比较对象内容
equals 方法必须满足自反性(x.equals(x)必须返回 true)、对称性(x.equals(y)返回 true 时,y.equals(x) 也必须返回 true)、传递性(x.equals(y)和 y.equals(z)都返回 true 时,x.equals(z)也必须返回 true)和一致性(当 x 和 y 引用的对象信息没有被修改时,多次调用 x.equals(y)应该得到同样的返回值),而且对于任何非 null值的引 用 x,x.equals(null)必须返回false。
- hashCode方法的作用?
从object类中继承过来的,用于鉴定两个对象是否相等,object类的hashcode返回对象在内存中地址转换成的一个int值,(对象变整型)
一般需要重写hashcode方法,在hsahmap中,可以用hashmap判断key是否重复。
如果两个对象的equals返回true,那他们的hashCode必须相等,但是hashCode相等,不一定equals不一定相等
如果两个对象 x 和 y 满足 x.equals(y) == true,它们的哈希码(hashCode)应当相同。 (1)如果两个对象相同(equals 方法返回 true),那么它们的hashCode 值一定要相同; (2)如果两个对象的 hashCode 相同,它们并不一定相同。 1. 使用==操作符检查"参数是否为这个对象的引用" 2. 使用 instanceof 操作符检查"参数是否为正确的类型"; 3. 对于类中的关键属性,检查参数传入对象的属性是否与之相匹配; 4. 编写完 equals 方法后,问自己它是否满足对称性、传递性、一致性; 5. 重写 equals 时总是要重写 hashCode; 6. 不要将 equals 方法参数中的 Object 对象替换为其他的类型,在重写时不要忘掉@Override 注解。
在实现 equals 时,我是先通过 getClass 方法判断两个对象的类型,你可能会想到还可以使用 instanceof 来判断。你能说说这两种实现方式的区别吗?
- instanceof进行类型检查规则是: 是该类或者是该类的子类;
- getClass获得类型信息采用==来进行检查是否相等的操作是严格的判断,不会存在继承方面的考虑
5.25、内部类和静态内部类的区别?
1、java中的内部类
- 1、静态内部类:类的静态成员,存在于某个类的内部
- 2、成员内部类:类的成员,存在于某个类的内部
- 成员内部类可以调用外部类的所有成员,但只有在创建了外部类的对象后,才能调用外部的成员
- 3、匿名内部类:存在于某个类的内部,是无类名的类
- 4、局部内部类:存在于某个方法的内部,只能在方法内部中使用,一旦方法执行完毕,局部内部类就会从内存中删除
- 必须注意:如果局部内部类中要使用他所在方法中的局部变量,那么就需要将这个局部变量定义为final的
2、各种内部类区别?
1、加载的顺序不同
- 静态内部类比内部类先加载
2、静态内部类被static修饰,在类加载时JVM会把它放到方法区,被本类以及本类中所有实例所公用。
- 定义在一个类内部的类叫内部类,内部类可以声明public、protected、private等访问限制,可以声明为abstract的供其他内部类或外部类继承与扩展,或者声明为static、final的,也可以实现特定的接口外部类按常规的类访问方式使用内部类
3、静态内部类只能够访问外部类的静态成员,而非静态内部类则可以访问外部类的所有成员(方法,属性)
- 非静态内部类不能有静态成员(方法、属性)
4、静态内部类和非静态内部类在创建时有区别
//假设类A有静态内部类B和非静态内部类C,创建B和C的区别为:
A a=new A(); A.B b=new A.B(); A.C c=a.new C();
5.26、Java中一个字符占多少个字节,扩展再问int, long, double占多少字节
一个字符两个字节, int 4 , long double 8
5.27、final/finally/finalize的区别?
1、final:可修饰属性,成员方法和类,表示属性不可变《引用不可变,对象可变》,方法不可覆盖,类不可继承 一般基本类型string stringbuffer都是不能被继承的
final的使用场景?
- 使用final修饰参数或者变量,也可以清楚地避免意外赋值导致的编程错误
- final变量产生了某种程度的不可变(immutable)的效果,所以,可以用于保护只读数据,尤其是在并发编程中
5.28 final和Immutable区别?
final只能约束strList这个引用不可以被赋值,但是strList对象行为不被final影响,可以添加元素等操作
Immutable在很多场景是非常棒的选择,某种意义上说,Java语言目前并没有原生的不可变支持,如果要实现immutable的类,我们需要做到:
1、将class自身声明为final,这样别人就不能扩展来绕过限制了。
2、将所有成员变量定义为private和fnal,并且不要实现setter方法。
3、通常构造对象时,成员变量使用深度拷贝来初始化,而不是直接赋值,这是一种防御措施,因为你无法确定输入对象不被其他人修改。
4、如果确实需要实现getter方法,或者其他可能会返回内部状态的方法,使用copy-on-write原则,创建私有的copy
为什么匿名内部类访问局部变量时,局部变量为什么使用final修饰?
因为java inner class实际会copy一份,不是去直接使用局部变量,final防止出现数据一致性问题
- 2、finally :异常处理的一部分,最终被执行(不要在finally代码块中处理返回值)(关闭jdbc连接,unlock操作)(政采云笔试做过这样的一道题)
1.当try,catch,finally中都有return语句时,无论try中的语句有无异常,均返回finally中的return。
public static int getStr() { try { int str = 1/0; return str; } catch (Exception e) { return 2; } finally { return 3; } } public static void main(String[] args) { System.out.println(getStr()); }
执行结果:
java.lang.ArithmeticException: / by zero at com.test.frame.fighting.application.getStr(application.java:14) at com.test.frame.fighting.application.main(application.java:25) 3 Process finished with exit code 0
-----------------------------------------当改成try中无异常时---------------
public static int getStr() { try { return 1; } catch (Exception e) { return 2; } finally { return 3; } } public static void main(String[] args) { System.out.println(getStr()); }
运行结果:
3 Process finished with exit code 0
finally的一道题?(finally不会被执行的情况)(政采云)
try { // do something System.exit(1);//1、try-catch异常退出 } fnally{ System.out.println(“Print from fnally”); }//finally里面的代码可不会被执行的哦,这是一个特例
2、无限循环
try{ while(ture){ print(abc) } }fnally{ print(abc) }
- 3、线程被杀死 当执行 try, finally 的线程被杀死时。 fnally 也无法执行
总结1:不要在finally中使用return语句。
2:finally总是执行,除非程序或者线程被中断。
finally的第二道题?
政采云笔试做过这样的一道题,在try catch finally中都有return语句,最后会返回哪个代码块的return语句?
当遇到return语句的时候,执行函数会立刻返回。但是,在Java语言中,如果存在finally就会有例外。除了return语句,try代码块中的break或continue语句
也可能使控制权进入fnally代码块。最后返回的是finally中的代码块。
注意点:如果在finally代码块中对函数返回的对象成员属性进行了修改,即使不在finally块中显式调用return语句,这个修改也会作用于返回值上
3、finalize obj的方法,被垃圾回收时会调用回收对象的finalize方法 可以重写此方法来提供垃圾收集时的其他资源回收,关闭文件等(在jdk被标记为deprecated)
缺点:1、无法保证finalize什么时候执行,执行的是否符合预期。使用不当会影响性能,导致程序死锁、挂起等。
一旦实现了非空的fnalize方法,就会导致相应对象回收呈现数量级上的变慢,有人专门做过benchmark,大概是40~50倍的下降(因为JVM要对它进行额外处理)
2、OOM原因之一:finalize拖慢垃圾收集,导致大量对象堆积。
3、finalize还会掩盖资源回收时的出错信息(JDK的源代码,截取自java.lang.ref.Finalizer)
private void runFinalizer(JavaLangAccess jla) { // ... 省略部分代码 try { Object fnalizee = this.get(); if (fnalizee != null && !(fnalizee insanceof java.lang.Enum)) { jla.invokeFinalize(fnalizee); // Clear sack slot containing this variable, to decrease // the chances of false retention with a conservative GC fnalizee = null; } } catch (Throwable x) { } super.clear(); }//这里的Throwable是被生吞了的!也就意味着一旦出现异常或者出错,你得不到任何有效信息
- 有什么机制可以替换finalize?(虚引用)
Java平台目前在逐步使用java.lang.ref.Cleaner来替换掉原有的fnalize实现,利用虚引用和引用队列,我们可以保证对象被彻底销毁前做一些类似资源回收的工作,
比如关闭文件描述符(操作系统有限的资源),它比fnalize更加轻量、更加可靠,Cleaner适合作为一种最后的保证手段,而不是完全依赖Cleaner进行资源回收
第三方库自己直接利用虚引用定制资源收集(比如广泛使用的MySQL JDBC driver之一的mysql-connector-j,就利用了虚引用机制。)
5.29、序列化的原理,序列化是怎么实现的?(20181113)
- 什么是序列化
序列化:指的是将java对象转换为二进制流的过程
反序列化:将二进制流恢复成对象的过程 - 序列化的解决方案:
java内置的序列化方式:效率较低
hessian:效率比protocal buffers稍低
json和xml:应用广泛 - 序列化的作用:
将对象通过网络传输到远端 - java内置序列化方式:(实现了serializable接口)
//关键代码 //定义一个字节数组输出流 ByteArrayOutputStream os = new ByteArrayOutputStream(); //对象输出流 ObjectOutputStream out = new ObjectOutputStream(os); //将对象写入到直接数组输出,进行序列化 out.writeObject(zhangsan);//将person类实例zhangsan序列化为字节数组 byte[] zhangsanByte = os.toByteArray(); //字节数组输入流 ByteArrayInputStream is = new ByteArrayInputStream(zhangsanByte); //执行反序列化,从流中读取对象 ObjectInputStream in = new ObjectInput(is); Persoon person = (Person)in.readObject();//反序列化 //Hessian序列化方案 需要引入提供的包hessian-4.0.7.jar,关键代码如下 ByteArrayOutputStream os = new ByteArrayOutputStream(); //hessian的序列化输出 HessianOutput ho = new HessianOutput(os); ho.writeObject(zhangsan);//将person类实例zhangsan序列化为字节数组 byte[] zhangsanByte = os.toByteArray(); //字节数组输入流 ByteArrayInputStream is = new ByteArrayInputStream(zhangsanByte); //执行反序列化,从流中读取对象 HessianInput in = new HessianInput(is); Persoon person = (Person)in.readObject();//反序列化
- 序列化的注意事项:(反序列化时的安全问题)
1、当一个父类实现序列化、子类自动实现序列化、不需要显示实现serializable接口
2、若该对象的实例变量引用其他对象,序列化该对象也把引用对象进行序列化
3、声明为static和transient类型的成员数据不能被序列化。因为static代表类的状态,transient代表对象的临时数据
4、序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,
该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类
5.30、Java源码
- 1、从jdk的工具包开始,也就是《数据结构与算法》java版,如list接口和arraylist、linkedlist实现,hashmap和treemap等。
这些数据结构也涉及到排序等算法。 - 2、core包
String、StringBuffer
如果有一定的javaio基础,可以读fileReader等类,可以看《java in a Nutshell》,里面有整个javaIO的架构图 - 3、javaIO包,是对继承和接口运用得最优雅的案例。如果将来做架构师,会经常与之打交道,如项目中部署和配置相关的核心类开发。读源码时,只需要读懂一些核心类即可,如和arraylist类似的二三十个类,对于每个类,也不一定要每个方法都读懂。像String有些方法已经到了虚拟机的层次(native方法),如hashcode方法。可以看看针对虚拟机的那套代码,如system classLoader的原理,他不在jdk包中,jdk是基于他的
- 4、java web开发源码
在阅读tomcat等源码之前,一定要有积累。
1、写过一些servlet和jsp的代码
2、看过《Servlet和JSP核心编程》
3、看过sun公司的servlet规范
4、看过http协议的rfc,debug过http的数据包
然后可以读struts2的源码,然后可以读tomcat的源码《how tomcat works》
他会告诉你httpServletRequest如何在容器内部实现的,tomcat如何通过socket来接受外面的请求,你的servlet代码如何被tomcat容器调用的(回调) - 5、java数据库源码阅读
先度sun公司JDBC的规范
mysql的jdbc驱动,因为他开源、设计优雅,如果你了解这些内幕,那么在学习hibernate和mybatis等持久化框架时,就会得心应手很多。
读完了jdbc驱动,可以读读数据库了,用java语言开发的数据库Hsqldb - 6、java通讯以及客户端软件
推荐即时通讯软件wildfire和spark。可以把wildfire理解成MSN服务器,Spark理解成MSN客户端。他们是通过XMPP协议通讯的。
原因:
1、XMPP轻量级,好理解
2、学习socket通讯实现,特别是C/S架构设计
3、模式化设计。他们都是基于module的,既可以了解模块化架构,还可以了解模块化的技术支撑:java虚拟机的classLoader的引用场景
4、Event Driven架构 - 7、java企业级应用
在读Spring源码前,一定要看rod johnson写的《j2ee design and development》,他是Spring的设计思路。
在读源码前,你会发现他们用到很多第三方jar包,最好把哪些jar包先一个个搞明白。
工作流:jbpm的源码,网上有介绍jbpm内核的文章,在读工作流源码前,一定要对其理论模型有深入的了解,以及写一些demo、或做过一些项目。
5.31 一个接口有多个实现类,当调用接口时,如何判断用的哪个实现类?202103补
- 1、直接new一个实例,这样肯定知道用的是哪个实例
- 2、定义接口类型的变量,用某个实例去初始化(常用)
举例子
A接口有个eat方法,A1、A2、A3分别实现A接口,A1吃饭 A2吃鱼 A3吃肉
需要得到“吃鱼”,A a = new A2();
需要得到“吃肉”,A a = new A3();
//接口: public interface CsBaseService { //获得总记录条数 public int getTotalCount(JDBCBean jdbcBean); } } //实现类1: @Service public class CsLastUpdateService implements CsBaseService { @Override public int getTotalCount(JDBCBean jdbcBean) { return 0; } } //实现类2: public class CsRelateModelService implements CsBaseService { @Override public int getTotalCount(JDBCBean jdbcBean) { return 2; } } //调用的时候: public class RelateModelController extends BaseController{ @Autowired private CsRelateModelService relateModelService;//自动装配实现类2 initParamProcess(relateModelService,new RelateModel(),new Page());//初始化实现类2,关键在这步,指定relateModelService为beaseService,具体见BaseController类 int totalCount = beaseService.getTotalCount(jdbcBean);//然后直接调用实现类2的方法,输出为2 } //抽象类 RelateModelController 的父类BaseController public abstract class BaseController { void initParamProcess(CsBaseService beaseService, JDBCBean jdbcBean,Page page) { this.beaseService = beaseService; //指定哪个实现类为beaseService this.jdbcBean = jdbcBean; this.page = page; } }
5.32、Java中的委派模式(Delegate)
委派模式(Delegate)是面向对象设计模式中常用的一种模式。
- 这种模式的原理为类B和类A是两个互相没有任何关系的类,B具有和A一模一样的方法和属性;并且调用B中的方法,属性就是调用A中同名的方法和属性。B好像就是一个受A授权委托的中介。第三方的代码不需要知道A的存在,也不需要和A发生直接的联系,通过B就可以直接使用A的功能,这样既能够使用到A的各种公能,又能够很好的将A保护起来了。一举两得,岂不很好!
- 下面用一个很简单的例子来解释下
class A{ void method1(){...} void method2(){...} } class B{ //delegation A a = new A(); //method with the same name in A void method1(){ a.method1();} void method2(){ a.method2();} //other methods and attributes ... } public class Test{ public static void main(String args[]){ B b = new B(); b.method1();//invoke method2 of class A in fact b.method2();//invoke method1 of class A in fact } }
5.33、采用单例模式还是静态方法?20210707补
观点一:(单例)
单例模式比静态方法有很多优势:
- 首先,单例可以继承类,实现接口,而静态类不能(可以集成类,但不能集成实例成员);
- 其次,单例可以被延迟初始化,静态类一般在第一次加载是初始化;
- 再次,单例类可以被集成,他的方法可以被覆写;
- 最后,或许最重要的是,单例类可以被用于多态而无需强迫用户只假定唯一的实例。举个例子,你可能在开始时只写一个配置,但是以后你可能需要支持超过一个配置集,或者可能需要允许用户从外部文件中加载一个配置对象,或者编写自己的。你的代码不需要关注全局的状态,因此你的代码会更加灵活。
观点二:(静态方法)
- 静态方法中产生的对象,会随着静态方法执行完毕而释放掉,而且执行类中的静态方法时,不会实例化静态方法所在的类。如果是用singleton, 产生的那一个唯一的实例,会一直在内存中,不会被GC清除的(原因是静态的属性变量不会被GC清除),除非整个JVM退出了。
观点三:(Good!)
- 由于DAO的初始化,会比较占系统资源的,如果用静态方法来取,会不断地初始化和释放,所以我个人认为如果不存在比较复杂的事务管理,用singleton会比较好。
总结:大家对这个问题都有一个共识:那就是实例化方法更多被使用和稳妥,静态方法少使用。
有时候我们对静态方法和实例化方法会有一些误解。
1、大家都以为“ 静态方法常驻内存,实例方法不是,所以静态方法效率高但占内存。”
- 事实上,他们都是一样的,在加载时机和占用内存上,静态方法和实例方法是一样的,在类型第一次被使用时加载。调用的速度基本上没有差别。
2、大家都以为“ 静态方法在堆上分配内存,实例方法在堆栈上”
- 事实上所有的方法都不可能在堆或者堆栈上分配内存,方法作为代码是被加载到特殊的代码内存区域,这个内存区域是不可写的。
方法占不占用更多内存,和它是不是static没什么关系。
因为字段是用来存储每个实例对象的信息的,所以字段会占有内存,并且因为每个实例对象的状态都不一致(至少不能认为它们是一致的),所以每个实例对象的所以字段都会在内存中有一分拷贝,也因为这样你才能用它们来区分你现在操作的是哪个对象。
但方法不一样,不论有多少个实例对象,它的方法的代码都是一样的,所以只要有一份代码就够了。因此无论是static还是non-static的方法,都只存在一份代码,也就是只占用一份内存空间。
同样的代码,为什么运行起来表现却不一样?这就依赖于方法所用的数据了。主要有两种数据来源,一种就是通过方法的参数传进来,另一种就是使用class的成员变量的值。
3、大家都以为“实例方法需要先创建实例才可以调用,比较麻烦,静态方法不用,比较简单”
- 事实上如果一个方法与他所在类的实例对象无关,那么它就应该是静态的,而不应该把它写成实例方法。所以所有的实例方法都与实例有关,既然与实例有关,那么创建实例就是必然的步骤,没有麻烦简单一说。
当然你完全可以把所有的实例方法都写成静态的,将实例作为参数传入即可,一般情况下可能不会出什么问题。
从面向对象的角度上来说,在抉择使用实例化方法或静态方法时,应该根据是否该方法和实例化对象具有逻辑上的相关性,如果是就应该使用实例化对象 反之使用静态方法。这只是从面向对象角度上来说的。
如果从线程安全、性能、兼容性上来看 也是选用实例化方法为宜。
我们为什么要把方法区分为:静态方法和实例化方法 ?
- 如果我们继续深入研 究的话,就要脱离技术谈理论了。早期的结构化编程,几乎所有的方法都是“静态方法”,引入实例化方法概念是面向对象概念出现以后的事情了,区分静态方法和 实例化方法不能单单从性能上去理解,创建c++、java、c# 这样面向对象语言的大师引入实例化方法一定不是要解决什么性能、内存的问题,而是为了让开发更加模式化、面向对象化。这样说的话,静态方法和实例化方式的区分是为了解决模式的问题。
5.34、贫血模型与充血模型 20210706补
6、Java IO
6.1、Java中的IO详解?
1、Java IO简介
传统的java.io包,它基于流模型实现,提供了我们熟知的一些IO功能,如File抽象、输入输出流等。交互方式是同步阻塞的,也就是:在读取输入流或者写入输出流时,读/写动作完成之前,线程会一直阻塞在那儿,调用顺序是线性的。
java.net提供的部分网络API:socket、serverSocket、HttpURLConnection也归类到同步阻塞IO类库,因为网络通信同样是IO行为。
6.2、Java中的IO有哪些?
按照流的方向:输入流(inputStream)和输出流(outputStream)
按照实现功能分:节点流(可以从或向一个特定的地方(节点)读写数据。如 FileReader)和处理流(是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写。如BufferedReader。处理流的构造方法总是要带一个其他的流对象做参数。一个流对象经过其他流的多次包装,称为流的链接)
6.3、字节流如何转为字符流 ?
字节输入流转字符输入流通过InputStreamReader实现,该类的构造函数可以传入InputStream对象
字节输出流转字符输出流通过OutputStreamWriter实现,该类的构造函数可以传入OutputStream对象
6.4、字节流和字符流的区别?
字节流读取的时候,读到一个字节就返回一个字节; 字节流可以处理所有类型数据
字符流使用了字节流读到一个或多个字节时。先去查指定的编码表,将查到的字符返回 字符流只能处理字符数据
只要是处理纯文本数据,就要优先考虑使用字符流,除此之外都用字节流
6.5、IO操作中需要注意的事项
1、IO不仅仅是对文件的操作,网络编程中,比如Socket通信,都是典型的IO操作目标。
2、输入流、输出流(InputStream/OutputStream)是用于读取或写入字节的,例如操作图片文件。
3、Reader/Writer则是用于操作字符,增加了字符编解码等功能,适用于类似从文件中读取或者写入文本信息。本质上计算机操作的都是字节,不管是网络通信还是文件读取,Reader/Writer相当于构建了应用逻辑和原始数据之间的桥梁。
4、BuferedOutputStream等带缓冲区的实现,可以避免频繁的磁盘读写,进而提高IO处理效率。这种设计利用了缓冲区,将批量数据进行一次操作,但在使用中千万别忘了flush。
5、很多IO工具类都实现了Closeable接口,因为需要进行资源的释放。比如,打开FileInputStream,它就会获取相应的文件描述符(FileDescriptor),需要利用try-with-resources、 try-finally等机制保证FileInputStream被明确关闭,进而相应文件描述符也会失效,否则将导致资源无法被释放。
7、单元测试
祸乱生于疏忽,单元测试先于交互。穿越暂时黑暗的时光隧道,才能迎来系统的曙光
7.1 单元测试的基本原则
7.2 公司里常用的单元测试方法
8、业务开发场景问题梳理
总体如下图所示
学习方式:
- 深入学习:对于每一个坑点,实际运行调试一下源码,使用文中提到的工具和方法重现问题;
- 眼见为实。对于每一个坑点,再思考下除了文内的解决方案和思路外,是否还有其他修正方式。对于坑点根因中涉及的 JDK 或框架源码分析,你可以找到相关类再系统阅读一下源码。
- 实践课后思考题。这些思考题,有的是对文章内容的补充,有的是额外容易踩的坑
参考资料:
1、https://www.jianshu.com/p/63e76826e852
2、《码出高效-java开发手册》
3、《Java 业务开发常见错误 100 例》
Action1:在一个 switch 块内, 每个 case 要么通过 continue / break / return 等来终止, 要么注释说明程序将继续执行到哪一个 case 为止; 在一个 switch 块内, 都必须包含一个 default 语句并且放在最后, 即使它什么代码也没有。
- 说明: 注意 break 是退出 switch 语句块, 而 return 是退出方法体
Action2:当 switch 括号内的变量类型为 String 并且此变量为外部参数时, 必须先进行 null 判断。
反例: 如下的代码输出是什么?
public class SwitchString { public static void main(String[] args) { method(null); } public static void method(String param) { switch (param) { // 肯定不是进入这里 case "sth": System.out.println("it's sth"); break; // 也不是进入这里 case "null": System.out.println("it's null"); break; // 也不是进入这里 default: System.out.println("default"); } } }
Action3:在 if / else / for / while / do 语句中必须使用大括号。
- 反例:
if (condition) statements;
- 说明: 即使只有一行代码,也要采用大括号的编码方式。
Action4:三目运算符 condition ? 表达式 1: 表达式 2 中,高度注意表达式 1 和 2 在类型对齐时,可能抛出因自动拆箱导致的 NPE 异常。
说明: 以下两种场景会触发类型对齐的拆箱操作:
- 1) 表达式 1 或 表达式 2 的值只要有一个是原始类型。
- 2)表达式 1 或 表达式 2 的值的类型不一致, 会强制拆箱升级成表示范围更大的那个类型。
反例:
Integer a = 1; Integer b = 2; Integer c = null; Boolean flag = false; // a*b 的结果是 int 类型, 那么 c 会强制拆箱成 int 类型, 抛出 NPE 异常 Integer result = (flag ? a * b : c);
Action5:在高并发场景中, 避免使用“等于” 判断作为中断或退出的条件。
说明: 如果并发控制没有处理好, 容易产生等值判断被“击穿” 的情况,使用大于或小于的区间判断条件来代替。
反例: 判断剩余奖品数量等于 0 时, 终止发放奖品, 但因为并发处理错误导致奖品数量瞬间变成了负数, 这样的话,
活动无法终止。
Action6:当方法的代码总行数超过 10 行时, return / throw 等中断逻辑的右大括号后需要加一个空行。
说明: 这样做逻辑清晰, 有利于代码阅读时重点关注
Action7:表达异常的分支时, 少用 if-else 方式, 这种方式可以改写成:
if (condition) { ... return obj; } // 接着写 else 的业务逻辑代码;
说明: 如果非使用 if()...else if()...else...
方式表达逻辑, 避免后续代码维护困难, 请勿超过 3 层。
正例: 超过 3 层的 if-else 的逻辑判断代码可以使用卫语句、 策略模式、 状态模式等来实现, 其中卫语句示例如下:
public void findBoyfriend(Man man) { if (man.isUgly()) { System.out.println("本姑娘是外貌协会的资深会员"); return; } if (man.isPoor()) { System.out.println("贫贱夫妻百事哀"); return; } if (man.isBadTemper()) { System.out.println("银河有多远, 你就给我滚多远"); return; } System.out.println("可以先交往一段时间看看"); }
Action8:除常用方法(如 getXxx / isXxx)等外,不要在条件判断中执行其它复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性。
说明: 很多 if 语句内的逻辑表达式相当复杂, 与、 或、 取反混合运算, 甚至各种方法纵深调用, 理解成本非常高。 如果赋
值一个非常好理解的布尔变量名字,则是件令人爽心悦目的事情。
正例:
// 伪代码如下 final boolean existed = (file.open(fileName, "w") != null) && (...) || (...); if (existed) { ... }
反例:
public final void acquire(long arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) { selfInterrupt(); } }
Action9:不要在其它表达式(尤其是条件表达式) 中,插入赋值语句。
说明: 赋值点类似于人体的穴位, 对于代码的理解至关重要, 所以赋值语句需要清晰地单独成为一行。
反例:
public Lock getLock(boolean fair) { // 算术表达式中出现赋值操作, 容易忽略 count 值已经被改变 threshold = (count = Integer.MAX_VALUE) - 1; // 条件表达式中出现赋值操作, 容易误认为是 sync == fair return (sync = fair) ? new FairSync() : new NonfairSync(); }
Action10:循环体中的语句要考量性能, 以下操作尽量移至循环体外处理, 如定义对象、 变量、 获取数据库连接, 进行不必要的 try-catch 操作(这个 try-catch 是否可以移至循环体外) 。
Action11:避免采用取反逻辑运算符。
说明: 取反逻辑不利于快速理解,并且取反逻辑写法一般都存在对应的正向逻辑写法。
正例: 使用 if(x < 628) 来表达 x 小于 628。
反例: 使用 if(!(x >= 628)) 来表达 x 小于 628。
Action12:公开接口需要进行入参保护,尤其是批量操作的接口。
反例: 某业务系统,提供一个用户批量查询的接口, API 文档上有说最多查多少个,但接口实现上没做任何保护,导致调用方传了一个 1000 的用户 id 数组过来后,查询信息后,内存爆了
Action13:下列情形, 需要进行参数校验:
1) 调用频次低的方法。
2) 执行时间开销很大的方法。此情形中,参数校验时间几乎可以忽略不计,但如果因为参数错误导致中间执行回退, 或者错误, 那得不偿失。
3) 需要极高稳定性和可用性的方法。
4) 对外提供的开放接口, 不管是 RPC / API / HTTP 接口。
5) 敏感权限入口
Action14:下列情形, 不需要进行参数校验:
1) 极有可能被循环调用的方法。 但在方法说明里必须注明外部参数检查。
2) 底层调用频度比较高的方法。毕竟是像纯净水过滤的最后一道,参数错误不太可能到底层才会暴露问题。 一般 DAO
层与 Service 层都在同一个应用中, 部署在同一台服务器中, 所以 DAO 的参数校验, 可以省略。
3) 被声明成 private 只会被自己代码所调用的方法, 如果能够确定调用方法的代码传入参数已经做过检查或者肯定不
会有问题, 此时可以不校验参数。