目录
ArrayList、Vector和LinkedList有什么共同点与区别?
HashSet中,equals与hashCode之间的关系?
String、StringBuffer、StringBuilder区别
为什么要把String设计为final不变量(不可被继承)?
String s = new String(“xyz”);创建了几个字符串对象?
JavaEE
JavaEE主要技术
JavaEE 号称有十三种核心技术。它们分别是:JDBC、JNDI、EJB、RMI、Servlet、JSP、XML、JMS、Java IDL、JTS、JTA、JavaMail和JAF。
JavaEE十三种规范
JAVAEE平台由一整套服务(Services)、应用程序接口(APIs)和协议构成,它对开发基于Web的多层应用提供了功能支持,下面对JAVAEE中的13种技术规范进行简单的描述。
1、JDBC(Java Database Connectivity)
JDBC API为访问不同的数据库提供了一种统一的途径,像ODBC一样,JDBC对开发者屏蔽了一些细节问题,另外,JDCB对数据库的访问也具有平台无关性。
2、JNDI(Java Naming and Directory Interface,Java命名和目录接口)
JNDI 是SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。
在没有JNDI之前:
开发的时候,在连接数据库代码中需要对JDBC驱动程序类进行应用,通过一个URL连接到数据库。但是这样存在问题,比如我要改一种数据库,是不是要更换驱动,更换URL。每次都要进行这些配置和管理。
在有了JNDI之后:
可以在J2ee容器中配置JNDI参数,定义一个数据源,在程序中,通过数据源名称引用数据源从而访问后台数据库。在程序中定义一个上下文类,然后用content.lookup("就可以成功引入数据源了。
在DataSource中事先建立多个数据库连接,保存在数据库连接池中,当程序访问数据库时,只用从连接池中取空闲状态的数据库连接即可,访问结束,销毁资源,数据库链接重新回到连接池
例如:数据源定义
3、EJB(Enterprise
EJB是sun的JavaEE服务器端组件模型,设计目标与核心应用是部署分布式应用程序。用通俗的话来理解,就是把已经打包好的东西放到服务器中去执行,这样是凭借了java跨平台的优势,利用EJB技术部署分布式系统可以不限于特定的平台。包括四种对象类型:无状态会话bean(提供独立服务),有状态会话bean(提供会话交互),实体bean(持久性数据在内存中的体现,服务器崩溃后可恢复),消息驱动bean。
EJB定义了服务器端组件是如何被编写以及提供了在组件和管理它们的服务器和组件间的标准架构协议.
4、RMI(Remote Method Invocation,远程方法调用)
RMI是Java的一组拥护开发分布式应用程序的API。RMI使用Java语言接口定义了远程对象,它集合了Java序列化和Java远程方法协议(。是一种被EJB使用的更底层的协议。
可以理解为,能够让在某个 Java 虚拟机上的对象调用另一个 Java 虚拟机中的对象上的方法。可以用此这个方法调用的任何对象必须实现该远程接口。
5、Java IDL/CORBA
Java IDL技术在Java平台上添加了CORBA(Common Object Request Broker Architecture)功能,提供了基于标准的操作能力和连接性。Java IDL技术使得分布式的Java Web应用能够通过使用工业标准的IDL和IIOP(Internet Inter-ORB Protocol)来透明地调用远程网络服务的操作。运行时组件(Runtime Components)包括了一个用于分布式计算且使用IIOP通信的Java ORB.我对这个规范的理解,它也是借用了java的集成,让新旧系统集成,或是客户端跨平台的使用。
6、JSP全名为Java Server Pages
中文名叫java服务器页面,其根本是一个简化的Servlet设计,它是由Sun Microsystems公司倡导、许多公司参与一起建立的一种动态网页技术标准。JSP的定义让我想到做BS项目时候的ASP.NET技术。JSP页面也是用HTML和JS的交互,服务器在页面被客户端所请求以后对这些Java代码进行处理,然后将生成的HTML页面返回给客户端的浏览器。
7、Java Servlet
一种J2EE组件,servlet可被认为是运行在服务器端的applet,Servlets提供了基于组件、平台无关的方法用以构建基本Web的应用程序。Servlet必须部署在Java servlet容器才能使用,为了在web容器里注册上面的Servlet,为应用建一个web.xml入口文件。servlets全部由Java写成并且生成HTML。
8、XML(Extensible Markup Language)
XML可扩展标记语言,标准通用标记语言的子集,是一种用于标记电子文件使其具有结构性的标记语言。近年来,随着 Web的应用越来越广泛和深入,人们渐渐觉得HTML不够用了,HTML过于简单的语法严重地阻碍了用它来表现复杂的形式。尽管HTML推出了一个又一个新版本,已经有了脚本、表格、帧等表达功能,但始终满足不了不断增长的需求。
有人建议直接使用SGML 作为Web语言,这固然能解决HTML遇到的困难。但是SGML太庞大了,用户学习和使用不方便尚且不说,要全面实现SGML的浏览器就非常困难,于是自然会想到仅使用SGML的子集,使新的语言既方便使用又实现容易。正是在这种形势下,Web标准化组织W3C建议使用一种精简的SGML版本——XML应运而生了。 XML的发展和Java是相互独立的,但是,它和Java具有的相同目标正是平台独立性。通过将Java和XML的组合,您可以得到一个完美的具有平台独立性的解决方案。
9、JMS即Java消息服务(Java Message Service)
JMS应用程序接口是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。用一个很形象的例子,如果有人请我吃饭,她给我打电话占线,她决定先去占个位置,但是如果没有短信技术,那么是不是我就不知道她给我的消息了呢?为了保证这样的异步通信,我可以看到短信,准时去赴约。JMS就是提供了这样一个面向消息的中间件。它们提供了基于存储和转发的应用程序之间的异步数据发送,即应用程序彼此不直接通信,而是与作为中介的MOM 通信。MOM提供了有保证的消息发送,应用程序开发人员无需了解远程过程调用(PRC)和网络/通信协议的细节。
10、JTA,即Java Transaction API
JTA允许应用程序执行分布式事务处理——在两个或多个网络计算机资源上访问并且更新数据。JDBC驱动程序的JTA支持极大地增强了数据访问能力。利用了事务处理,可以让数据等到同步的更新,技术上可以支持多个服务器的分布式访问。
11、JTS(Java Transaction Service)
JTS是CORBA OTS事务监控的基本的实现。JTS规定了事务管理器的实现方式。该事务管理器是在高层支持Java Transaction API (JTA)规范,并且在较底层实现OMG OTS specification的Java映像。JTS事务管理器为应用服务器、资源管理器、独立的应用以及通信资源管理器提供了事务服务。
12、JavaMail
JavaMail API提供了一种独立于平台和独立于协议的框架来构建邮件和消息传递应用程序。不仅支持SMTP服务器,也支持IMAP服务器。是一个提供给使用Java平台的开发者处理电子邮件有关的编程接口。
13、JAF(JavaBeans Activation Framework)
JavaMail利用JAF来处理MIME编码的邮件附件。MIME的字节流可以被转换成Java对象,或者转换自Java对象。大多数应用都可以不需要直接使用JAF
javaSE、javaEE、javaME区别
参考:
(60条消息) J2EE是什么(一)_小兀哥的博客-CSDN博客_j2ee
(60条消息) J2EE架构简介_崔耀强的博客-CSDN博客_j2ee Java、JavaEE、JavaSE、JavaME、JavaWEB之间的区别与联系分 - 知乎 (zhihu.com)
Java 2平台版本:
概述:
javaME(微型版):适用于小型设备和智能卡的Java 2平台Micro版
javaSE(标准版):适用于桌面系统的Java 2平台标准版
javaEE(企业版):适用于创建服务器应用程序和服务的Java 2平台企业版
PS:Java2是1.2以后的版本。2EE全称为Java2 Platform Enterprise Edition。“J2EE平台本质上是一个分布式的服务器应用程序设计环境——一个Java环境,它提供了:宿主应用的一个运行基础框架环境和一套用来创建应用的Java扩展API。”
详述:
Java是一门编程语言。Java分为三大版本,SE即标准版,包含了Java核心类库,主要用来开发桌面应用;EE即企业版,包含SE,又有扩展部分(Servlet,JDBC等),主要用来开发分布式网络程序;ME即微型版,包含了SE中部分类库,又有自己扩展部分,主要用来做移动类、嵌入式开发。
JavaSE(JavaPlatform,StandardEdition)。JavaSE以前称为J2SE。它允许开发和部署在桌面、服务器、嵌入式环境和实时环境中使用的Java应用程序。JavaSE包含了支持JavaWeb服务开发的类,并为JavaPlatform,EnterpriseEdition(JavaEE)提供基础。
JavaEE(JavaPlatform,EnterpriseEdition)。这个版本以前称为J2EE。企业版本帮助开发和部署可移植、健壮、可伸缩且安全的服务器端Java应用程序。JavaEE是在JavaSE的基础上构建的,它提供Web服务、组件模型、管理和通信API,可以用来实现企业级的面向服务体系结构(service-orientedarchitecture,SOA)和Web2.0应用程序。
JavaME(JavaPlatform,MicroEdition)。这个版本以前称为J2ME。JavaME为在移动设备和嵌入式设备(比如手机、PDA、电视机顶盒和打印机)上运行的应用程序提供一个健壮且灵活的环境。JavaME包括灵活的用户界面、健壮的安全模型、许多内置的网络协议以及对可以动态下载的连网和离线应用程序的丰富支持。基于JavaME规范的应用程序只需编写一次,就可以用于许多设备,而且可以利用每个设备的本机功能。
JavaWeb:
JavaWeb是指使用Java体系开发网站类应用,JSP属于JavaWeb范畴,JSP可以简单看作是前端页面嵌入Java代码,会被容器编译成Servlet,然后Servlet会输出HTML代码,最终成为我们看到的页面。
J2EE体系结构:
(60条消息) J2EE架构简介_崔耀强的博客-CSDN博客_j2ee
访问修饰符
类内部 | 本包 | 子类 | 外部包 | |
public | √ | √ | √ | √ |
protected | √ | √ | √ | |
default | √ | √ | ||
private | √ |
关键字
概述
1.什么是关键字
JAVA关键字就是Java语言中已经被赋予特定意义的一些单词。
怎么理解呢?你可以设想一下你家新装修买了一扇门,你能叫这扇门为”天安门”吗?显然不能,在这里”天安门”就是关键字,就是一个被赋予了特殊意义的词。
注意事项:Java关键字是区分大小的!java的关键字都是小写的。所以void是关键字,但Void就不是了,public是关键字Public就不是了~~
2.常用关键字
48个关键字:abstract、assert、boolean、break、byte、case、catch、char、class、continue、default、do、double、else、enum、extends、final、finally、float、for、if、implements、import、int、interface、instanceof、long、native、new、package、private、protected、public、return、short、static、strictfp、super、switch、synchronized、this、throw、throws、transient、try、void、volatile、while
参考:Java中的关键字有哪些?「Java中53个关键字的意义及使用方法」_java关键字-CSDN博客
final
在Java中声明类、属性和方法时,可使用关键字final来修饰。
final标记的类不能被继承;
final标记的方法不能被子类复写;
final标记的变量(成员变量或局部变量)即为常量,只能赋值一次。
final关键字修饰类、成员变量和成员方法
1.final类
final用来修饰一个类,意味着该类成为不能被继承的最终类。出于安全性的原因和效率上的考虑,有时候需要防止一个类被继承。例如,Java类库中的String类,它对编译器和解释器的正常运行有着很重要的作用,不能轻易改变它,因此把它修饰为final类,使它不能被继承,这就保证了String类的惟一性。同时,如果你认为一个类的定义己经很完美,不需要再生成它的子类,这时也应把它修饰为final类。
定义一个final类的格式如下:
final class ClassName{
...
}
注意:声明为final的类隐含地声明了该类的所有方法为final方法。
下面的结论是成立的:声明一个类既为abstract,又为final是非法的,因为抽象类必须被子类继承来实现它的抽象方法。下面是一个final类的例子:
final class A{
...
}
class B extends A{//错误!不能继承A
...
}
2.final修饰成员变量
变量被声明为final后,成为常值变量(即常量),一旦被通过某种方式初始化或赋值,即不能再被修改。通常static与final一起使用来指定一个类常量。例如:
static final int SUNDAY=0;
把final变量用大写字母和下划线来表示,这是一种编码规定。
3.final修饰成员方法
用final修饰的方法为最终方法,不能再被子类重写,可以被重载。
尽管方法重写是Java非常有力的特征,但有时却需要避免这种情况的发生。为了不允许一个方法被重写,在方法的声明中指定final属性即可。例如:
class A{
final void method(){}
}
class B extends A{//定义A类的一个子类B
void method(){}//错误,method()不能被重写
}
该例中,因为method()方法在A中被声明为final,所以不能在子类B中被重写。如果这样做,将导致编译错误。
方法被声明为final有时可以提高性能:编译器可以自由地内联调用它们,因为它“知道”它们不会被子类重写。
static
static的主要作用:
1. 为某种特定数据类型或对象分配与创建对象个数无关的单一的存储空间。
2. 使得某个方法或属性与类而不是对象关联在一起,即在不创建对象的情况下可通过类直接调用方法或使用类的属性。
static 4种使用方式:
1.修饰变量(只能修饰成员变量,不能用于修饰局部变量)
用static关键字修饰的静态变量在内存中只有一个副本。只要静态变量所在的类被加载,这个静态变量就会被分配空间,可以使用''类.静态变量''和''对象.静态变量''的方法使用。
2. 修饰成员方法:
static修饰的方法无需创建对象就可以被调用。static方法中不能使用this和super关键字,不能调用非static方法,只能访问所属类的静态成员变量和静态成员方法。
3. 修饰代码块:
JVM在加载类的时候会执行static代码块。static代码块常用于初始化静态变量。static代码块只会被执行一次。
静态代码块是不需要依赖main方法就可以独立运行的。
在类被加载的时候运行且只运行一次。
4. 修饰内部类:
static内部类可以不依赖外部类实例对象而被实例化。静态内部类不能与外部类有相同的名字,不能访问普通成员变量,只能访问外部类中的静态成员和静态成员方法。
什么时候用static
当一个方法或者变量需要初始化加载,或者是经常被调用的时候可以加上static。
坏处:初始化加载,比较占内存,所以不经常用的方法,不建议加此关键字。
如果static是写在单例中,高并发访问是会出问题的,这时候就要设置线程等待了,static是在容器加载的时候就已经加载到内存中,所以static方法和变量不宜过度使用,有选择的使用。
如果你需要通过计算来初始化你的static变量,你可以声明一个static块,Static 块仅在该类被加载时执行一次。
什么是成员变量
成员变量就是对象的属性:
static为什么一般与final一起用?
static和final的意义是不同的,
static修饰的时候代表对象是静态的,而final修饰的时候代表对象只能赋值一次,
他们连用的时候是因为定义的那个对象既要它是静态的,也要求它的值不能再被修改。
举例说明:
static int a=1;
static final b=1;
这里a和b的区别在于,a在程序里可以被重新赋值为2或3或等等的整数,而b在程序里不能被重新赋值,b永远都为1,也就是说b是一个常量。
final int c=1;
static final b=1;
这里c和b的区别在于,b存放在静态空间,不会在程序运行时被释放,它永远占着内存直到程序终止,而c在程序用完它而不会再用到它的时候就会被自动释放,不再占用内存。
当一个常数或字符串我们需要在程序里反复反复使用的时候,我们就可以把它定义为static final,这样内存就不用重复的申请和释放空间。
native
Java平台有个用户和本地C代码进行互操作的API,称为Java Native Interface,简称JNI(Java本地接口)。
1、什么是Native Method
native 用来修饰方法,用 native 声明的方法表示告知 JVM 调用,该方法在外部定义,我们可以用任何语言去实现它。 简单地讲,一个native Method就是一个 Java 调用非 Java 代码的接口。
简单地讲,一个Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C。这个特征并非java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern "C"告知C++编译器去调用一个C的函数。
如果一个含有本地方法的类被继承,子类会继承这个本地方法并且可以用java语言重写这个方法(这个似乎看起来有些奇怪),同样的如果一个本地方法被fianl标识,它被继承后不能被重写。
本地方法非常有用,因为它有效地扩充了jvm。事实上,我们所写的java代码已经用到了本地方法,在sun的java的并发(多线程)的机制实现中,许多与操作系统的接触点都用到了本地方法,这使得java程序能够超越java运行时的界限。有了本地方法,java程序可以做任何应用层次的任务。
2、为什么要使用Native Method
有时java应用需要与java外面的环境交互。这是本地方法存在的主要原因。你可以想想java需要与一些底层系统如操作系统或某些硬件交换信息时的情况。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解java应用之外的繁琐的细节。
3、与操作系统交互
JVM支持着java语言本身和运行时库(windows的命令行窗口),它是java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。然而不管怎样,它毕竟不是一个完整的系统,它经常依赖于一些底层(underneath在下面的)系统的支持。这些底层系统常常是强大的操作系统。通过使用本地方法,我们得以用java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用C写的,还有,如果我们要使用一些java语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。
Sun's Java
Sun的解释器是用C实现的,这使得它能像一些普通的C一样与外部交互。jre大部分是用java实现的,它也通过一些本地方法与外界交互。例如:类java.lang.Thread 的 setPriority()方法是用java实现的,但是它实现调用的是该类里的本地方法setPriority0()。这个本地方法是用C实现的,并被植入JVM内部,在Windows 95的平台上,这个本地方法最终将调用Win32 SetPriority() API。这是一个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库(动态链接库在操作系统中)(external dynamic link library)提供,然后被JVM调用。
native 语法:
①、修饰方法的位置必须在返回类型之前,和其余的方法控制符前后关系不受限制。
②、不能用 abstract 修饰,也没有方法体,也没有左右大括号。
③、返回值可以是任意类型
我们在日常编程中看到native修饰的方法,只需要知道这个方法的作用是什么,至于别的就不用管了,操作系统会给我们实现。
使用 @Native 注解修饰成员变量,则表示这个变量可以被本地代码引用,常常被代码生成工具使用。对于 @Native 注解不常使用,了解即可。
含有本地方法的类,一般都会在静态代码块中初始化加载调用本地方法。
public class Thread implements Runnable {
/* Make sure registerNatives is the first thing <clinit> does. */
private static native void registerNatives();
static {
registerNatives();
}
}
volatile
参考:
关键字: volatile详解 | Java 全栈知识体系 (pdai.tech)
主要作用
- 内存可见性:是指当一个线程修改了某个变量的值,其它线程总是能知道这个变量变化。也就是说,如果线程 A 修改了共享变量 V 的值,那么线程 B 在使用 V 的值时,能立即读到 V 的最新值。
- 禁止指令重排序:使用 volatile 变量进行写操作,汇编指令带有 lock 前缀,相当于一个内存屏障,编译器不会将后面的指令重排到内存屏障之前。
- 保证原子性:单次读/写;
什么是重排序?为了提高性能,在遵守 as-if-serial 语义(即不管怎么重排序,单线程下程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守。)的情况下,编译器和处理器常常会对指令做重排序。
一般重排序可以分为如下三种类型:
- 编译器优化重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行重排序:现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统重排序:由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
i++是一个复合操作,包括三步骤:
- 读取i的值。
- 对i加1。
- 将i的值写回内存。
volatile是无法保证这三个操作是具有原子性的,我们可以通过AtomicInteger或者Synchronized来保证+1操作的原子性。
内存可见性
内存可见性(Memory Visibility)是指当某个线程正在使用对象状态而另一个线程在同时修改该状态,需要确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。
可见性错误是指当读操作与写操作在不同的线程中执行时,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。
我们可以通过同步来保证对象被安全地发布。除此之外我们也可以使用一种更加轻量级的 volatile 变量。
Java 提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知到其他线程。可以将 volatile 看做一个轻量级的锁,但是又与锁有些不同:
- 对于多线程,不是一种互斥关系
- 不能保证变量状态的“原子性操作
问题代码示例
/**
* @ClassName TestVolatile
* @Description: Thread 已经修改了flag,但是main线程还是拿到的false
* @Author: WangWenpeng
* @Version 1.0
*/
public class TestVolatile {
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo();
new Thread(td).start();
while (true) {
if (td.isFlag()) {
System.out.println("______________");
break;
}
}
}
}
class ThreadDemo implements Runnable {
private boolean flag = false;
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
try {
//增加这种出现问题的几率
Thread.sleep(200);
} catch (Exception e) {
}
flag = true;
System.out.println("flag=" + isFlag());
}
}
两个线程同时修改这一个flag,为什么main拿到的还是这种修改之前的值
内存分析
解决方法,加锁
public class TestVolatile {
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo();
new Thread(td).start();
while (true) {
synchronized (td) {
if (td.isFlag()) {
System.out.println("______________");
break;
}
}
}
}
}
加了锁,就可以让while循环每次都从主存中去读取数据,这样就能读取到true了。但是一加锁,每次只能有一个线程访问,当一个线程持有锁时,其他的就会阻塞,效率就非常低了。不想加锁,又要解决内存可见性问题,那么就可以使用volatile关键字。
volatile
private volatile boolean flag = false;
volatile 关键字:当多个线程进行操作共享数据时,可以保证内存中的数据可见。相较于 synchronized 是一种较为轻量级的同步策略。
volatile 的原子性问题
所谓原子性就是操作不可再细分。
所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。
在多线程环境下,volatile 关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性。也就是说,多线程环境下,使用 volatile 修饰的变量是线程不安全的。
要解决这个问题,我们可以使用锁机制,或者使用原子类(如 AtomicInteger)。
这里特别说一下,对任意单个使用 volatile 修饰的变量的读 / 写是具有原子性,但类似于 flag = !flag 这种复合操作不具有原子性。简单地说就是,单纯的赋值操作是原子性的。
问题代码
package com.atguigu.juc;
/**
* @ClassName TestAtomicDemo
* @Description:
* @Author: WangWenpeng
* @Version 1.0
*/
public class TestAtomicDemo {
public static void main(String[] args) {
AtomicDemo ad = new AtomicDemo();
for (int i = 0; i < 10; i++) {
new Thread(ad).start();
}
}
}
class AtomicDemo implements Runnable {
private int serialNumber = 0;
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + ":" + getSerialNumber());
}
public int getSerialNumber() {
return serialNumber++;
}
}
看到这里,好像和上面的内存可见性问题一样。是不是加个volatile关键字就可以了呢?其实不是的,因为加了volatile,只是相当于所有线程都是在主存中操作数据而已,但是不具备互斥性。比如两个线程同时读取主存中的0,然后又同时自增,同时写入主存,结果还是会出现重复数据。
import java.util.concurrent.atomic.AtomicInteger;
/**
* @ClassName TestAtomicDemo
*
* 原子变量:在 java.util.concurrent.atomic 包下提供了一些原子变量。
* 1. volatile 保证内存可见性
* 2. CAS(Compare-And-Swap) 算法保证数据变量的原子性
* CAS 算法是硬件对于并发操作的支持
* CAS 包含了三个操作数:
* ①内存值 V
* ②预估值 A
* ③更新值 B
* 当且仅当 V == A 时, V = B; 否则,不会执行任何操作。
*
* @Description:
* @Author: WangWenpeng
* @Version 1.0
*/
public class TestAtomicDemo {
public static void main(String[] args) {
AtomicDemo ad = new AtomicDemo();
for (int i = 0; i < 10; i++) {
new Thread(ad).start();
}
}
}
class AtomicDemo implements Runnable {
private AtomicInteger serialNumber = new AtomicInteger(0);
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + ":" + getSerialNumber());
}
public int getSerialNumber() {
return serialNumber.getAndIncrement();
}
}
AtomicInteger这个玩意是具有原子性的integer,用它替换后发现能保证线程安全
Connected to the target VM, address: '127.0.0.1:61323', transport: 'socket'
Thread-4:1
Thread-6:4
Thread-0:3
Thread-7:9
Thread-2:2
Thread-5:6
Thread-3:5
Thread-1:0
Thread-9:7
Thread-8:8
Disconnected from the target VM, address: '127.0.0.1:61323', transport: 'socket'
volatile 修饰符适用场景
- 某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值;或者作为状态变量,如 flag = ture,实现轻量级同步。
- volatile 属性的读写操作都是无锁的,它不能替代 synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的。
- volatile 只能作用于属性,我们用 volatile 修饰属性,这样编译器就不会对这个属性做指令重排序。
- volatile 提供了可见性,任何一个线程对其的修改将立马对其他线程可见。
- volatile 提供了 happens-before 保证,对 volatile 变量 V 的写入 happens-before 所有其他线程后续对 V 的读操作。
- volatile 可以使纯赋值操作是原子的,如 boolean flag = true; falg = false。
- volatile 可以在单例双重检查中实现可见性和禁止指令重排序,从而保证安全性。
注意
- volatile 不具备“互斥性”;
- volatile 不能保证完全的原子性,只能保证单次的读/写操作具有原子性;
如何保证多线程下共享变量的可见性?
加锁 和 使用 volatile 关键字。
加锁,如使用synchronized 关键字,synchronized 有两个作用,一个是保证线程同步执行,二是synchronized 在释放锁的时候会刷新缓存中的数据,从主存重新读取数据。保证变量的内存可见性用到的是synchronized 的第二个作用。
使用 volatile 和 synchronized 锁都可以保证共享变量的可见性。相比 synchronized 而言,volatile 可以看作是一个轻量级锁,所以使用 volatile 的成本更低,因为它不会引起线程上下文的切换和调度。但 volatile 无法像 synchronized 一样保证操作的原子性。
synchronized
参考:
关键字: synchronized详解 | Java 全栈知识体系 (pdai.tech)
特点
- synchronized 可以保证代码的原子性和可见性;
性质
可重入(可以避免死锁、单个线程可以重复拿到某个锁,锁的粒度是线程而不是调用)、不可中断(其实也就是上面的原子性)
分类
- 按照作用对象划分为 对象锁、类锁;
- 按照作用位置划分为 代码块、方法(静态和非静态);
- 按照具体细节划分为 实例(普通方法)同步方法、静态同步方法、实例方法中的同步代码块、静态方法中的代码块;
如果从类是 Class 对象的角度看,类锁也是对象锁,但上面这样划分的解释性更好。
使用
- 直接修饰某个实例方法,锁是当前实例对象;
- 直接修饰某个静态方法,锁是当前类的class对象;
- 修饰代码块,锁是括号里面的对象;
注意
- 一把锁只能同时被一个线程获取,没有获得锁的线程只能等待;
- 每个实例都对应有自己的一把锁(this),不同实例之间互不影响;例外:锁对象是*.class以及synchronized修饰的是static方法的时候,所有对象公用同一把锁;
- synchronized修饰的方法,无论方法正常执行完毕还是抛出异常,都会释放锁;
对象锁
包括方法锁(默认锁对象为this,当前实例对象)和同步代码块锁(自己指定锁对象)。
-
代码块形式:手动指定锁定对象,也可是是this,也可以是自定义的锁
-
方法锁形式:synchronized修饰普通方法,锁对象默认为this
类锁
指synchronize修饰静态的方法或指定锁对象为Class对象。
-
synchronize修饰静态方法;
-
synchronized指定锁对象为Class对象;
多线程中 synchronized 锁升级
原理:
在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。
目的:
锁升级是为了减低了锁带来的性能消耗。
在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。
底层实现原理
加解锁原理、可重入原理、可见性原理;
Java 对象底层都关联一个的 monitor(监视器),使用 synchronized 时 JVM 会根据使用环境找到对象的 monitor,根据 monitor 的状态进行加解锁的判断。如果成功加锁就成为该 monitor 的唯一持有者, monitor 在被释放前不能再被其他线程获取。
synchronized 在JVM编译后会产生monitorenter 和 monitorexit 这两个字节码指令,获取和释放monitor。这两个字节码指令都需要一个引用类型的参数指明要锁定和解锁的对象,对于同步普通方法,锁是当前实例对象;对于静态同步方法,锁是当前类的 Class 对象;对于同步方法块,锁是synchronized 括号里的对象。
执行 monitorenter 指令时,首先尝试获取对象锁。如果这个对象没有被锁定,或当前线程已经持有锁,就把锁的计数器加 1,执行 monitorexit 指令时会将锁计数器减 1。一旦计数器为 0 锁随即就被释放。
图说明可见性原理:
JMM 是 Java 内存模型的缩写,直接看图。
相关问题
synchronized 可以作用在哪里? 分别通过对象锁和类锁进行举例。
synchronized 是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
- 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
- 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
- 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
- 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象。
synchronized 本质上是通过什么保证线程安全的? 分三个方面回答:加锁和释放锁的原理,可重入原理,保证可见性原理。
synchronized 有什么样的缺陷? JavaLock是怎么弥补这些缺陷的。
- 效率低:锁的释放情况少,只有代码执行完毕或者异常结束才会释放锁;试图获取锁的时候不能设定超时,不能中断一个正在使用锁的线程,相对而言,Lock可以中断和设置超时。
- 不够灵活:加锁和释放的时机单一,每个锁仅有一个单一的条件(某个对象),相对而言,读写锁更加灵活。
- 无法知道是否成功获得锁,相对而言,Lock可以拿到状态,如果成功获取锁,....,如果获取失败,....
synchronized 在使用时有何注意事项?
- 锁对象不能为空,因为锁的信息都保存在对象头里;
- 作用域不宜过大,影响程序执行的速度,控制范围过大,编写代码也容易出错;
- 避免死锁;
- 在能选择的情况下,既不要用Lock也不要用synchronized关键字,用java.util.concurrent包中的各种各样的类,如果不用该包下的类,在满足业务的情况下,可以使用synchronized关键,因为代码量少,避免出错;
synchronized 使得同时只有一个线程可以执行,性能较差,有什么方法可以提升性能?
- 优化 synchronized 的使用范围,让临界区的代码在符合要求的情况下尽可能的小。
- 使用其他类型的 lock(锁),synchronized 使用的锁经过 jdk 版本的升级,性能已经大幅提升了,但相对于更加轻量级的锁(如读写锁)还是偏重一点,所以可以选择更合适的锁。
synchronized 修饰的方法在抛出异常时,会释放锁吗?
会。
synchronized是公平锁吗?
- synchronized实际上是非公平的,新来的线程有可能立即获得监视器,而在等待区中等候已久的线程可能再次等待,这样有利于提高性能,但是也可能会导致饥饿现象。
多个线程等待同一个 synchronized 锁的时候,JVM如何选择下一个获取锁的线程?
这个问题就涉及到内部锁的调度机制,线程获取 synchronized 对应的锁,也是有具体的调度算法的,这个和具体的虚拟机版本和实现都有关系,所以下一个获取锁的线程是事先没办法预测的。
什么是锁的升级和降级?
JDK6 之后,不断优化 synchronized,提供了三种锁的实现,分别是偏向锁、轻量级锁、重量级锁,还提供自动的升级和降级机制。对于不同的竞争情况,会自动切换到合适的锁实现。当没有竞争出现时,默认使用偏斜锁,也即是在对象头的 Mark Word 部分设置线程ID,来表示锁对象偏向的线程,但这并不是互斥锁;当有其他线程试图锁定某个已被偏斜过的锁对象,JVM 就撤销偏斜锁,切换到轻量级锁,轻量级锁依赖 CAS 操作对象头的 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则进一步升级为重量级锁。锁的降级发生在当 JVM 进入安全点后,检查是否有闲置的锁,并试图进行降级。锁的升级和降级都是出于性能的考虑。
什么是JVM里的偏斜锁、轻量级锁、重量级锁?
偏向锁:在线程竞争不激烈的情况下,减少加锁和解锁的性能损耗,在对象头中保存获得锁的线程ID信息,如果这个线程再次请求锁,就用对象头中保存的ID和自身线程ID对比,如果相同,就说明这个线程获取锁成功,不用再进行加解锁操作了,省去了再次同步判断的步骤,提升了性能。
轻量级锁:再线程竞争比偏向锁更激烈的情况下,在线程的栈内存中分配一段空间作为锁的记录空间(轻量级锁对应的对象的对象头字段的拷贝),线程通过CAS竞争轻量级锁,试图把对象的对象头字段改成指向锁记录的空间,如果成功就说明获取轻量级锁成功,如果失败,则进入自旋(一定次数的循环,避免线程直接进入阻塞状态)试图获取锁,如果自旋到一定次数还不能获取到锁,则进入重量级锁。
自旋锁:获取轻量级锁失败后,避免线程直接进入阻塞状态而采取的循环一定次数去尝试获取锁。(线程进入阻塞状态和非阻塞状态都是涉及到系统层面的,需要在用户态到内核态之间切换,非常消耗系统资源)实验证明,锁的持有时间一般是非常短的,所以一般多次尝试就能竞争到锁。
重量级锁:在 JVM 中又叫做对象监视器(monitor),锁对象的对象头字段指向的是一个互斥量,多个线程竞争锁,竞争失败的线程进入阻塞状态(操作系统层面),并在锁对象的一个等待池中等待被唤醒,被唤醒后的线程再次竞争锁资源。
不同的JDK中对 synchronized 有何优化?
参考:
Synchronized 中的 4 个优化,你知道几个? - 知乎 (zhihu.com)
synchronized 在 JDK 1.5 时性能是比较低的,然而在后续的版本中经过各种优化迭代,它的性能也得到了前所未有的提升,之前说到过锁膨胀对 synchronized 性能的提升,然而它也只是“众多” synchronized 性能优化方案中的一种,那么我们本文就来盘点一下 synchronized 的核心优化方案。
JDK 1.6 之前,synchronized 是重量级锁,也就是说 synchronized 在释放和获取锁时都会从用户态转换成内核态,而转换的效率是比较低的。但有了锁膨胀机制之后,synchronized 的状态就多了无锁、偏向锁以及轻量级锁了,这时候在进行并发操作时,大部分的场景都不需要用户态到内核态的转换了,这样就大幅的提升了 synchronized 的性能。
synchronized 核心优化方案主要包含以下 4 个:
- 锁膨胀
- 锁消除
- 锁粗化
- 自适应自旋锁
锁膨胀和自适应自旋锁是 synchronized 关键字自身的优化实现,而锁消除和锁粗化是 JVM 虚拟机对 synchronized 提供的优化方案,这些优化方案最终使得 synchronized 的性能得到了大幅的提升,也让它在并发编程中占据了一席之地。
当一个线程进入一个对象的synchronized方法A之后,其它线程是否可进入此对象的synchronized方法B?
不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。因为非静态方法上的synchronized修饰符要求执行方法时要获得对象的锁,如果已经进入A方法说明对象锁已经被取走,那么试图进入B方法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁。
synchronized 和Lock的对比和选择?
对比:
Lock是Java 5以后引入的新的API,和关键字synchronized相比主要相同点:Lock 能完成synchronized所实现的所有功能;主要不同点:Lock有比synchronized更精确的线程语义和更好的性能,而且不强制性的要求一定要获得锁。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且最好在finally 块中释放(这是释放外部资源的最好的地方)。
选择:
一切选择都要根据具体的业务需要,选择更合适的方式,减少出错。如果有现成的包(java.util.concurrent)和类(原子类、ConcurrentHashMap、CountDownLatch),就不要自己造轮子了,直接拿来用,Java 原生支持的效率也会更高些,如果真的要用到 Lock 或者 Synchronized 独有的特性,再来考虑这两个。
我想更加灵活的控制锁的释放和获取(现在释放锁和获取锁的时机都被规定死了),怎么办?
可以根据需要实现一个 Lock 接口,这样锁的获取和释放就能完全被我们控制了。
synchronized 和 ReentrantLock 区别
- synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。
- 既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:
- ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁;
- ReentrantLock可以获取各种锁的信息;
- ReentrantLock可以灵活地实现多路通知;
- 另外,二者的锁机制其实也是不一样的:ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word。
synchronized 和 volatile 的区别
- volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
- volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
synchronized 与 Lock 的区别
补充:
- 首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
- synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
- synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
- 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
- synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可);
- Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码量少的同步问题。
abstract
1.abstract类中可以有abstract方法
abstract类中可以有abstract方法,也可以有非abstract方法
非abstract类中不可以有abstract方法
2.abstract类不能使用new运算符创建对象
但是如果一个非抽象类是抽象类的子类,这时候我们想要创建该对象呢,这时候它就必须要重写父类的抽象方法,并且给出方法体,这也就是说明了为什么不允许使用final和abstract同时修饰一个类或者方法的原因。
重点常考!:final和abstract,private和abstract,static和abstract,这些是不能放在一起的修饰符,因为abstract修饰的方法是必须在其子类中实现(覆盖),才能以多态方式调用,以上修饰符在修饰方法时期子类都覆盖不了这个方法,final是不可以覆盖,private是不能够继承到子类,所以也就不能覆盖,static是可以覆盖的,但是在调用时会调用编译时类型的方法,因为调用的是父类的方法,而父类的方法又是抽象的方法,又不能够调用,所以上的修饰符不能放在一起。
3.abstract类的子类
如果一个非abstract类是abstract类的子类,它必须重写父类的abstract方法,也就是去掉abstract方法的abstract修饰,并给出方法体。
如果一个abstract类是abstract类的子类,它可以重写父类的abstract方法,也可以继承父类的abstract方法。
super
在上一节中曾经提到过super的使用,那super到底是什么呢?super关键字出现在子类中,我们new子类的实例对象的时候,子类对象里面会有一个父类对象。怎么去引用里面的父类对象呢?使用super来引用,所以可以得出结论:super主要的功能是完成子类调用父类中的内容,也就是调用父类中的属性或方法。
super关键字的使用
super关键字的用法如下:
super可以用来引用直接父类的实例变量。
super可以用来调用直接父类方法。
super()可以用于调用直接父类构造函数。
1.super用于引用直接父类实例变量
public class TestSuper1 {
public static void main(String args[]) {
Dog d = new Dog();
d.printColor();
}
}
class Animal {
String color = "white";
}
class Dog extends Animal {
String color = "black";
void printColor() {
System.out.println(color);// prints color of Dog class
System.out.println(super.color);// prints color of Animal class
}
}
输出结果:
black
white
在上面的例子中,Animal和Dog都有一个共同的属性:color。 如果我们打印color属性,它将默认打印当前类的颜色。要访问父属性,需要使用super关键字指定。
2.通过super来调用父类方法
public class TestSuper2 {
public static void main(String args[]) {
Dog d = new Dog();
d.work();
}
}
class Animal {
void eat() {
System.out.println("eating...");
}
}
class Dog extends Animal {
void eat() {
System.out.println("eating bread...");
}
void bark() {
System.out.println("barking...");
}
void work() {
super.eat();
bark();
}
}
输出结果:
eating...
barking...
在上面的例子中,Animal和Dog两个类都有eat()方法,如果要调用Dog类中的eat()方法,它将默认调用Dog类的eat()方法,因为当前类的优先级比父类的高。所以要调用父类方法,需要使用super关键字指定。
3.使用super来调用父类构造函数
public class TestSuper3 {
public static void main(String args[]) {
Dog d = new Dog();
}
}
class Animal {
Animal() {
System.out.println("animal is created");
}
}
class Dog extends Animal {
Dog() {
super();
System.out.println("dog is created");
}
}
输出结果:
animal is created
dog is created
注意:如果没有使用super()或this(),则super()在每个类构造函数中由编译器自动添加。
super与this关键字的比较
super关键字:我们可以通过super关键字来实现对父类成员的访问,用来引用当前对象的父类。
this关键字:指向自己的引用。
范例:
public class TestAnimalDogDemo {
public static void main(String[] args) {
Animal a = new Animal();
a.eat();
Dog d = new Dog();
d.eatTest();
}
}
class Animal {
void eat() {
System.out.println("animal : eat");
}
}
class Dog extends Animal {
void eat() {
System.out.println("dog : eat");
}
void eatTest() {
this.eat(); // this 调用自己的方法
super.eat(); // super 调用父类方法
}
}
输出结果:
animal : eat
dog : eat
animal : eat
上表对 this与 super 的差别进行了比较,从上表中不难发现,用 super或this 调用构造方法时都需要放在首行,所以super 与 this 调用构造方法的操作是不能同时出现的。
package case1;
import java.util.Scanner;
public class Task1 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String dogName = sc.next();
String dogSex = sc.next();
String dogColor = sc.next();
String catName = sc.next();
String catSex = sc.next();
double catWeight = sc.nextDouble();
// 通过有参构造函数实例化Dog类对象dog
// dog调用talk()方法
// dog调用eat()方法
/********* begin *********/
Dog dog = new Dog(dogName, dogSex, dogColor);
dog.talk();
dog.eat();
/********* end *********/
// 通过有参构造函数实例化Cat类对象cat
// cat调用talk()方法
// cat调用eat()方法
/********* begin *********/
Cat cat = new Cat(catName, catSex, catWeight);
cat.talk();
cat.eat();
/********* end *********/
}
}
// 抽象类Pet 封装属性name和sex
// 构造函数初始化name和sex
// 声明抽象方法talk()
// 声明抽象方法eat()
abstract class Pet {
/********* begin *********/
private String name;
private String sex;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public Pet(String name, String sex) {
this.name = name;
this.sex = sex;
}
public abstract void talk();
public abstract void eat();
/********* end *********/
}
// Dog类继承自Pet类 封装属性color
// 构造函数初始化name、sex和color
// 实现自己的talk()方法和eat()方法
// talk()输出'名称:name,性别:sex,颜色:color,汪汪叫'
// eat()输出'name吃骨头'
class Dog extends Pet {
/********* begin *********/
private String color;
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public Dog(String name, String sex, String color) {
super(name, sex);
this.color = color;
}
public void talk() {
System.out.println("名称:" + this.getName() + ",性别:" + this.getSex()
+ ",颜色:" + this.getColor() + ",汪汪叫");
}
public void eat() {
System.out.println(this.getName() + "吃骨头!");
}
/********* end *********/
}
// Cat类继承自Pet类 封装属性weight
// 构造函数初始化name、sex和weight
// 实现自己的talk()方法和eat()方法
// talk()输出'名称:name,性别:sex,体重:weight kg,喵喵叫'
// eat()输出'name吃鱼'
class Cat extends Pet {
/********* begin *********/
private double weight;
public double getWeight() {
return weight;
}
public void setWeight(double weight) {
this.weight = weight;
}
public Cat(String name, String sex, double weight) {
super(name, sex);
this.weight = weight;
}
public void talk() {
System.out.println("名称:" + this.getName() + ",性别:" + this.getSex()
+ ",体重:" + this.getWeight() + "kg" + ",喵喵叫");
}
public void eat() {
System.out.println(this.getName() + "吃鱼!");
}
/********* end *********/
this
This调用类的构造方法时:
只能在构造方法中使用this调用其他构造方法,不能在成员方法中使用。
在构造方法中使用this调用构造方法时必须位于第一行,且只能出现一次。
不能在一个类的两个构造方法中使用this相互调用。
dafault
default关键字:是在java 8中引入的新概念,也可称为Virtual extension methods——虚拟扩展方法与public、private等都属于修饰符关键字,与其它两个关键字不同之处在于default关键字大部分都用于修饰接口。
default修饰方法时只能在接口类中使用,在接口中被default标记的方法可以直接写方法体,而无需修改所有实现了此接口的类。
这打破了Java之前版本对接口的语法限制。之前的版本里 interface 中的方法必须是抽象方法,不能有方法体。现在可以添加 interface 内方法,只需要在方法的前面加一个 default 关键字,表示属于接口内部默认存在的方法,但是,实现类中可以重写其实现,也可以不重写。在调用default修饰的方法时,如果没有匹配的实现类实现它,就执行接口类中定义的默认实现逻辑;否则,执行实现类中的逻辑。
类中的方法优先于接口。
泛型
参考:
Java进阶 - 泛型(面试题)持续更新..... - 掘金 (juejin.cn)
简述泛型
什么是泛型?泛型的作用?泛型的优点?
泛型,即“参数化类型”,解决不确定对象具体类型的问题。在编译阶段有效。在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型在类中称为泛型类、接口中称为泛型接口和方法中称为泛型方法。
泛型是JDK 1.5引入的新特性,那么Java之所以引入它我认为主要有三个作用
- 类型检查,它将运行时类型转换的ClassCastException通过泛型提前到编译时期。
- 避免类型强转。
- 泛型可以泛型算法,增加代码的复用性。
- 使用泛型参数,可以增强代码的可读性以及稳定性。编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如 ArrayList<Persion> persons = new ArrayList<String>() 这行代码就指明了该ArrayList 对象只能传入 Persion 对象,如果传入其他类型的对象就会报错。
可以用于构建泛型集合。原生 List 返回类型是 Object ,需要手动转换类型才能使用,使用泛型后编译器自动转换。
简述泛型擦除
Java的泛型是JDK5新引入的特性,为了向下兼容,虚拟机其实是不支持泛型,所以Java实现的是一种 伪泛型机制,也就是说Java在编译期擦除了所有的泛型信息,这样Java就不需要产生新的类型到字节码, 所有的泛型类型最终都是一种原始类型,在Java运行时根本就不存在泛型信息。
Java编译器生成的字节码是不包涵泛型信息的,泛型类型信息将在编译处理是被擦除,这个过程被称为泛型擦除。
java编译器具体是如何擦除泛型的?
- 检查泛型类型,获取目标类型;
- 擦除类型变量,并替换为限定类型 如果泛型类型的类型变量没有限定(),则用Object作为原始类型 如果有限定(),则用XClass作为原始类型 如果有多个限定(T extends XClass1&XClass2),则使用第一个边界XClass1作为原始类;
- 在必要时插入类型转换以保持类型安全;
- 生成桥方法以在扩展时保持多态性;
为什么泛型被擦除但是仍然可以通过反射拿到?
通过创建内部类/继承它的方式来声明所需要的反序列化Bean
对象的实际泛型类型。
使用方式
泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。
1.泛型类://此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey(){
return key;
}
}
如何实例化泛型类:
Generic<Integer> genericInteger = new Generic<Integer>(123456);
2.泛型接口 :
public interface Generator<T> {
public T method();
}
实现泛型接口,不指定类型:
class GeneratorImpl<T> implements Generator<T>{
@Override
public T method() {
return null;
}
}
实现泛型接口,指定类型:
泛型约束和局限性
- 不能实例化类型变量(因为泛型只存在于编译期,而泛型的具体类型在运行期才可以知道,所以无法初始化)
- 静态域或者方法里不能引用类型变量(因为静态不依赖类的实例化,而泛型的类型确认需要类实例化才可得知)
- 实例化泛型类型,必须是引用类型,因为类型擦除后就变成了Object
- 不能使用
instanceof
关键字判断泛型的类型(语法规定编译不能通过) - 不管泛型传入的类型是什么,泛型类编译后getClass().getName()得到的都是泛型类的原始类型,这句话很绕,但很好理解,就是下面代码的GenericTest<T>不管T传入的是什么,那么最终编译的GenericTest的实例都是 new GenericTest().
- 可以声明泛型数组但是不能实例化(因为类型擦除后,引用类型数组就变成了Object [])
- 泛型类不能extends Exception/Throwable 原因
- 不能捕获泛型类对象 原因
- 不能使用泛型定义重载方法(因为形参类型擦除后,他们就都变成了原始类型)
集合
集合的特点:
用来存储不同类型的对象(基本数据类型除外),存储长度可变。
Java集合中实际存放的只是对象的引用,每个集合元素都是一个引用变量,实际内容都放在堆内存或者方法区里面,但是基本数据类型是在栈内存上分配空间的,栈上的数据随时就会被收回的。
基本类型数据如何解决呢?
可以通过包装类把基本类型转为对象类型,存放引用就可以解决这个问题。更方便的,由于有了自动拆箱和装箱功能,基本数据类型和其对应对象(包装类)之间的转换变得很方便,想把基本数据类型存入集合中,直接存就可以了,系统会自动将其装箱成封装类,然后加入到集合当中。
- byte ~> Byte
- short ~> Short
- boolean ~> Boolean
- char ~> Char
- int ~> Integer
- long ~> Long
- float ~> Float
- double ~> Double
集合框架图:
简述JAVA的List
- 可以允许元素重复。
- 可以插入多个null元素。
- 是一个有序容器,保持了每个元素的插入顺序,输出的顺序就是插入的顺序。
- 常用的实现类有 ArrayList、LinkedList 和 Vector。ArrayList 最为流行,它提供了使 用索引的随意访问,而 LinkedList 则对于经常需要从 List 中添加或删除元素的场合更 为合适。
List是一个有序队列,在JAVA中有两种实现方式:
ArrayList 使用数组实现,是容量可变的非线程安全列表,随机访问快,集合扩容时会创建更大的数组,把原有数组复制到新数组。
LinkedList 本质是双向链表,与 ArrayList 相比插入和删除速度更快,但随机访问元素很慢。
简述CopyOnWriteArrayList
参考:
Java并发编程:并发容器之CopyOnWriteArrayList(转载) - Matrix海子 - 博客园 (cnblogs.com)
简述JAVA的Set
- 不允许元素重复;
- 无序容器,无法保证每个元素的存储顺序,TreeSet通过 Comparator 或者 Comparable 维护了一个排序顺序;
- 只允许一个 null 元素;
- Set 接口最流行的几个实现类是 HashSet、LinkedHashSet 以及 TreeSet;
java是在Set建立和往其中加元素时保证的Set的中元素不重复,但如果元素已经放入集合中,再通过指针修改其中的元素,Set是无法保证其中元素被修改后仍然不重复的。
为了维护Set集合中的元素不重复的特性,我们可以遵循以下原则:
避免更改已经加入 Set 集合中的对象的属性值。如果需要更改对象的属性值,可以先从集合中删除该对象,然后更改属性值后再将其重新添加到集合中。
在定义用于比较对象是否相等的 equals 和 hashCode 方法时,可以考虑使用不可变的属性作为比较依据。这样,即使对象的其他属性发生了变化,也不会影响到对象在 Set 集合中的唯一性。
如果需要在 Set 集合中存储可变对象,并且需要频繁更改对象的属性值,可以考虑使用其他数据结构,例如 List 或 Map,来替代 Set 集合。
最流行的是基于 HashMap 实现的 HashSet:
JAVA对Set有三种实现方式:HashSet 通过 HashMap 实现,HashMap 的 Key 即 HashSet 存储的元素,Value系统自定义一个名为PRESENT 的 Object 类型常量。判断元素是否相同时,先比较hashCode,相同后再利用equals比较,查询O(1)
LinkedHashSet 继承自 HashSet,通过 LinkedHashMap 实现,使用双向链表维护元素插入顺序。
TreeSet 通过 TreeMap 实现的,底层数据结构是红黑树,添加元素到集合时按照比较规则将其插入合适的位置,保证插入后的集合仍然有序。查询O(logn)
TreeSet 还实现了 SortedSet 接口,因此TreeSet 是一个根据其 compare() 和 compareTo() 的定义进行排序的有序容器。
HashSet如何保证元素不重复?
HashSet 底层是由 HashMap 实现的,它可以实现重复元素的去重功能,如果存储的是自定义对象必须重写 hashCode 和 equals 方法。HashSet 保证元素不重复是利用 HashMap 的 put 方法实现的,在存储之前先根据 key 的 hashCode 和 equals 判断是否已存在,如果存在就不在重复插入了,这样就保证了元素的不重复。
既然hashset基于hashmap实现,你说一下 hashset的add方法中,为什么要在map.put的val上放上一个object类型的静态常量present?
首先要看hashmap的put方法的返回值,map对象在调用put的时候传入一个key和val,会对其key进行一个算法得到一个位置,会把put的数据放到其位置上,如果该位置上已经存在当前key,会对其key映射的val给替换掉,并且返回之前的val,则返回null。
好了,既然put的返回值原理搞清楚了,就要去看看 set 的add方法的实现,add方法是调用了put方法,并且把key放在了put的key上,val放了一个hashset类的静态常量present,如果put 返回的是null,不是present,就说明 put的key是不存在的,add也会返回true,如果put返回的是present就说明之前的key是存在的,并不是说没有put上,所以add方法返回的false并不是存失败的意思,而是map.put的key是已经存在的,而且已经把val给替换了。
简述JAVA的HashMap
简述
-
不是collection的子接口或者实现类。Map是一个接口。
-
Map 的 每个 Entry 都持有两个对象,也就是一个键一个值,Map 可能会持有相同的值对象但键对象必须是唯一的。
-
TreeMap 也通过 Comparator 或者 Comparable 维护了一个排序顺序。
-
Map 里你可以拥有随意个 null 值但最多只能有一个 null 键。
-
Map 接口最流行的几个实现类是 HashMap、LinkedHashMap、Hashtable 和 TreeMap。(HashMap、TreeMap最常用)
JDK8 之前底层实现是数组 + 链表,JDK8 改为数组 + 链表/红黑树。主要成员变量包括存储数据的table 数组、元素数量 size、加载因子 loadFactor。
HashMap 中数据以键值对的形式存在,键对应的 hash 值用来计算数组下标,如果两个元素 key 的hash 值一样,就会发生哈希冲突,被放到同一个链表上。
table 数组记录 HashMap 的数据,每个下标对应一条链表,所有哈希冲突的数据都会被存放到同一条链表,Node/Entry 节点包含四个成员变量:key、value、next 指针和 hash 值。在JDK8后链表超过8会转化为红黑树。
若当前数据/总数据容量>负载因子,Hashmap将执行扩容操作。
默认初始化容量为 16,扩容容量必须是 2 的幂次方、最大容量为 1<< 30 、默认加载因子为 0.75。
原理详解
重要变量介绍:
ps:都是重要的变量记忆理解一下最好。
DEFAULT_INITIAL_CAPACITY
Table数组的初始化长度:1 << 4
2^4=16
(为什么要是 2的n次方?)MAXIMUM_CAPACITY
Table数组的最大长度:1<<30
2^30=1073741824
DEFAULT_LOAD_FACTOR
负载因子:默认值为0.75
。 当元素的总个数>当前数组的长度 * 负载因子。数组会进行扩容,扩容为原来的两倍(todo:为什么是两倍?)TREEIFY_THRESHOLD
链表树化阙值: 默认值为8
。表示在一个node(Table)节点下的值的个数大于8时候,会将链表转换成为红黑树。UNTREEIFY_THRESHOLD
红黑树链化阙值: 默认值为6
。 表示在进行扩容期间,单个Node节点下的红黑树节点的个数小于6时候,会将红黑树转化成为链表。MIN_TREEIFY_CAPACITY = 64
最小树化阈值,当Table所有元素超过改值,才会进行树化(为了防止前期阶段频繁扩容和树化过程冲突)。
实现原理:
实现原理图 我们都知道,在HashMap中,采用数组+链表的方式来实现对数据的储存。
HashMap采⽤Entry数组来存储key-value对,每⼀个键值对组成了⼀个Entry实体,Entry类实际上是⼀个单向的链表结 构,它具有Next指针,可以连接下⼀个Entry实体。 只是在JDK1.8中,链表⻓度⼤于8的时候,链表会转成红⿊树!
为什么使用链表+数组:要知道为什么使用链表首先需要知道Hash冲突是如何来的?
答: 由于我们的数组的值是限制死的,我们在对key值进行散列取到下标以后,放入到数组中时,难免出现两个key值不同,但是却放入到下标相同的格子中,此时我们就可以使用链表来对其进行链式的存放。
⽤LinkedList代替数组结构可以吗?
对于题目的意思是说,在源码中我们是这样的
Entry[] table=new Entry[capacity];
// entry就是一个链表的节点
现在进行替换,进行如下的实现
List<Entry> table=new LinkedList<Entry>();
是否可以行得通? 答案当然是肯定的。
既然可以进行替换处理,为什么偏偏使用到数组呢?
因为⽤数组效率最⾼! 在HashMap中,定位节点的位置是利⽤元素的key的哈希值对数组⻓度取模得到。此时,我们已得到节点的位置。显然数组的查 找效率⽐LinkedList
⼤(底层是链表结构)。
那ArrayList
,底层也是数组,查找也快啊,为啥不⽤ArrayList? 因为采⽤基本数组结构,扩容机制可以⾃⼰定义,HashMap中数组扩容刚好是2的次幂,在做取模运算的效率⾼。 ⽽ArrayList的扩容机制是1.5倍扩容(这一点我相信学习过的都应该清楚),那ArrayList为什么是1.5倍扩容这就不在本⽂说明了。
Hash冲突:得到下标值:
我们都知道在HashMap中 使用数组加链表,这样问题就来了,数组使用起来是有下标的,但是我们平时使用HashMap都是这样使用的:
HashMap<Integer,String> hashMap=new HashMap<>();
hashMap.put(2,"dd");
可以看到的是并没有特地为我们存放进来的值指定下标,那是因为我们的hashMap对存放进来的key值进行了hashcode(),生成了一个值,但是这个值很大,我们不可以直接作为下标,此时我们想到了可以使用取余的方法,例如这样:
key.hashcode()%Table.length;
即可以得到对于任意的一个key值,进行这样的操作以后,其值都落在0-Table.length-1
中,但是 HashMap的源码却不是这样做?
它对其进行了与操作,对Table的表长度减一再与生产的hash值进行相与:
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
我们来画张图进行进一步的了解;
hashmap 容量为什么是 2 的幂次?
这里我们也就得知为什么Table数组的长度要一直都为2的n次方
,只有这样,减一进行相与时候,才能够达到最大的n-1
值。
举个栗子来反证一下:
我们现在 数组的长度为 15 减一为 14 ,二进制表示 0000 1110
进行相与时候,最后一位永远是0,这样就可能导致,不能够完完全全的进行Table数组的使用。违背了我们最开始的想要对Table数组进行最大限度的无序使用的原则,因为HashMap为了能够存取高效,,要尽量较少碰撞,就是要尽量把数据分配均匀,每个链表⻓度⼤致相同。
此时还有一点需要注意的是: 我们对key值进行hashcode以后,进行相与时候都是只用到了后四位,前面的很多位都没有能够得到使用,这样也可能会导致我们所生成的下标值不能够完全散列。解决方案:
将生成的hashcode值的高16位于低16位进行异或运算,这样得到的值再进行相与,一得到最散列的下标值。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
为什么hashmap的在链表元素数量超过8时候改为红黑树?
- 知道jdk1.8中hashmap改了什么吗。
- 说一下为什么会出现线程的不安全性
- 为什么在解决hash冲突时候,不直接用红黑树,而是先用链表,再用红黑树
- 当链表转为红黑树,什么时候退化为链表
第一问改动了什么
1.由数组+链表的结构改为数组+链表+红⿊树。
2. 优化了⾼位运算的hash算法:h^(h>>>16)
3. 扩容后,元素要么是在原位置,要么是在原位置再移动2次幂的位置,且链表顺序不变。
注意: 最后⼀条是重点,因为最后⼀条的变动,hashmap在1.8中,不会在出现死循环问题。
HashMap的并发问题
HashMap在并发环境下会有什么问题?一般是如何解决的?
问题的出现
- (1)多线程扩容,引起的死循环问题
- (2)多线程put的时候可能导致元素丢失
- (3)put⾮null元素后get出来的却是null
不安全性的解决方案
- 在之前使用hashtable。 在每一个函数前面都加上了synchronized 但是 效率太低 我们现在不常用了。
- 使用 ConcurrentHashmap函数,对于这个函数而言 我们可以每几个元素共用一把锁。用于提高效率。
HashMap的key
一般用什么作为HashMap的key值?
- key可以是null吗,value可以是null吗
- 一般用什么作为key值
- 用可变类当Hashmap1的Key会有什么问题
- 让你实现一个自定义的class作为HashMap的Key该如何实现
key可以是null吗,value可以是null吗?
当然都是可以的,但是对于 key来说只能运行出现一个key值为null,但是可以出现多个value值为null。
一般用什么作为key值?
⼀般⽤Integer、String这种不可变类当HashMap当key,⽽且String最为常⽤。
(1)因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。 这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。 这就是HashMap中的键往往都使⽤字符串。
(2)因为获取对象的时候要⽤到equals()和hashCode()⽅法,那么键对象正确的重写这两个⽅法是⾮常重要的,这些类已 经很规范的覆写了hashCode()以及equals()⽅法。
可变类当Hashmap1的Key会有什么问题?
hashcode可能会发生变化,导致put进行的值,无法get出来,如下代码所示:
HashMap<List<String>,Object> map=new HashMap<>();
List<String> list=new ArrayList<>();
list.add("hello");
Object object=new Object();
map.put(list,object);
System.out.println(map.get(list));
list.add("hello world");
System.out.println(map.get(list));
输出值如下:
java.lang.Object@1b6d3586
null
实现一个自定义的class作为Hashmap的key该如何实现?
对于这个问题考查到了下面的两个知识点
- 重写hashcode和equals方法需要注意什么?
- 如何设计一个不变的类。
针对问题⼀,记住下⾯四个原则即可
(1)两个对象相等,hashcode⼀定相等
(2)两个对象不等,hashcode不⼀定不等
(3)hashcode相等,两个对象不⼀定相等
(4)hashcode不等,两个对象⼀定不等
针对问题⼆,记住如何写⼀个不可变类
(1)类添加final修饰符,保证类不被继承。 如果类可以被继承会破坏类的不可变性机制,只要继承类覆盖⽗类的⽅法并且继承类可以改变成员变量值,那么⼀旦⼦类 以⽗类的形式出现时,不能保证当前类是否可变。
(2)保证所有成员变量必须私有,并且加上final修饰 通过这种⽅式保证成员变量不可改变。但只做到这⼀步还不够,因为如果是对象成员变量有可能再外部改变其值。所以第4 点弥补这个不⾜。
(3)不提供改变成员变量的⽅法,包括setter 避免通过其他接⼝改变成员变量的值,破坏不可变特性。
(4)通过构造器初始化所有成员,进⾏深拷⻉(deep copy)
(5) 在getter⽅法中,不要直接返回对象本⾝,⽽是克隆对象,并返回对象的拷⻉ 这种做法也是防⽌对象外泄,防⽌通过getter获得内部可变成员对象后对成员变量直接操作,导致成员变量发⽣改变
后记
- 对于HashMap而言,扩容是一个特别消耗内存的操作。所以当程序员在使用HashMap的时候,估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容。
- 负载因子是可以修改的,也可以大于1,但是建议不要轻易修改,除非情况非常特殊。
- HashMap是线程不安全的,不要在并发的环境中同时操作HashMap,建议使用ConcurrentHashMap。
简述JAVA的TreeMap
TreeMap是底层利用红黑树实现的Map结构,底层实现是一棵平衡的排序二叉树,由于红黑树的插入、删除、遍历时间复杂度都为O(logN),所以性能上低于哈希表。但是哈希表无法提供键值对的有序输出,红黑树可以按照键的值的大小有序输出。
MultiKeyMap的使用
参考:
集合工具类Collections
Collections:集合框架的工具类。里面定义的都是静态方法。
Collections和Collection有什么区别?
Collection是集合框架中的一个顶层接口,它里面定义了单列集合的共性方法。
它有两个常用的子接口,
——List:对元素都有定义索引。有序的。可以重复元素。
——Set:不可以重复元素。无序。
Collections是集合框架中的一个工具类。该类中的方法都是静态的。
提供的方法中有可以对list集合进行排序,二分查找等方法。
通常常用的集合都是线程不安全的。因为要提高效率。
如果多线程操作这些集合时,可以通过该工具类中的同步方法,将线程不安全的集合,转换成安全的。
集合区别
Iterator和Iterable的区别:
- Iterator是迭代器接口,而Iterable是为了只要实现该接口就可以使用foreach,进行迭代。
- Iterable中封装了Iterator接口,只要实现了Iterable接口的类,就可以使用Iterator迭代器了。
- 集合Collection、List、Set都是Iterable的实现类,所以他们及其他们的子类都可以使用foreach进行迭代。
- Iterator中和核心的方法next(),hasnext(),remove(),都是依赖当前位置,如果这些集合直接实现Iterator,则必须包括当前迭代位置的指针。当集合在方法间进行传递的时候,由于当前位置不可知,所以next()之后的值,也不可知。而当实现Iterable则不然,每次调用都返回一个从头开始的迭代器,各个迭代器之间互不影响。
Collection和Collections有什么区别?
1. Collection是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的子类,比如List、Set等。
2. Collections是一个包装类,包含了很多静态方法、不能被实例化,而是作为工具类使用,比如提供的排序方法:
Collections.sort(list);提供的反转方法:Collections.reverse(list)。
ArrayList、Vector和LinkedList有什么共同点与区别?
1. ArrayList、Vector和LinkedList都是可伸缩的数组,即可以动态改变长度的数组。
2. ArrayList和Vector都是基于存储元素的Object[] array来实现的,它们会在内存中开辟一块连续的空间来存储,支持下标、索引访问。但在涉及插入元素时可能需要移动容器中的元素,插入效率较 低。当存储元素超过容器的初始化容量大小,ArrayList与Vector均会进行扩容。
3. Vector是线程安全的,其大部分方法是直接或间接同步的。ArrayList不是线程安全的,其方法不具有同步性质。LinkedList也不是线程安全的。
4. LinkedList采用双向列表实现,对数据索引需要从头开始遍历,因此随机访问效率较低,但在插入元素的时候不需要对数据进行移动,插入效率较高。
HashMap和Hashtable有什么区别?
1. HashMap是Hashtable的轻量级实现,HashMap允许key和value为null,但最多允许一条记录的key 为null,而HashTable不允许。
2. HashTable中的方法是线程安全的,而HashMap不是。在多线程访问HashMap需要提供额外的同步机制。
3. Hashtable使用Enumeration进行遍历,HashMap使用Iterator进行遍历。
如何决定使用HashMap还是TreeMap?
如果对Map进行插入、删除或定位一个元素的操作更频繁,HashMap是更好的选择。如果需要对key集合进行有序的遍历,TreeMap是更好的选择。
HashSet中,equals与hashCode之间的关系?
equals和hashCode这两个方法都是从object类中继承过来的,equals主要用于判断对象的内存地址引用是否是同一个地址;hashCode根据定义的哈希规则将对象的内存地址转换为一个哈希码。HashSet中存储的元素是不能重复的,主要通过hashCode与equals两个方法来判断存储的对象是否相同:
1. 如果两个对象的hashCode值不同,说明两个对象不相同。
2. 如果两个对象的hashCode值相同,接着会调用对象的equals方法,如果equlas方法的返回结果为true,那么说明两个对象相同,否则不相同。
PO、BO、VO、DTO、POJO、DAO的区别
1、entity里的每一个字段,与数据库相对应,
2、dto里的每一个字段,是和你前台页面相对应,
3、VO,这是用来转换从entity到dto,或者从dto到entity的中间的东西。
(一) PO:
persistant object持久对象
最形象的理解就是一个PO就是数据库中的一条记录。
好处是可以把一条记录作为一个对象处理,可以方便的转为其它对象。
(二) BO:
business object业务对象
主要作用是把业务逻辑封装为一个对象。这个对象可以包括一个或多个其它的对象。
比如一个简历,有教育经历、工作经历、社会 关系等等。
我们可以把教育经历对应一个PO,工作经历对应一个PO,社会 关系对应一个PO。
建立一个对应简历的BO对象处理简历,每个BO包含这些PO。
这样处理业务逻辑时,我们就可以针对BO去处理。
(三) VO :
value object值对象
ViewObject表现层对象
主要对应界面显示的数据对象。对于一个WEB页面,或者SWT、SWING的一个界面,用一个VO对象对应整个界面的值。
(四) DTO :
Data Transfer Object数据传输对象
主要用于远程调用等需要大量传输对象的地方。
比如我们一张表有100个字段,那么对应的PO就有100个属性。
但是我们界面上只要显示10个字段,
客户端用WEB service来获取数据,没有必要把整个PO对象传递到客户端,
这时我们就可以用只有这10个属性的DTO来传递结果到客户端,这样也不会暴露服务端表结构.到达客户端以后,如果用这个对象来对应界面显示,那此时它的身份就转为VO
(五) POJO :
plain ordinary java object 简单java对象
个人感觉POJO是最常见最多变的对象,是一个中间对象,也是我们最常打交道的对象。
一个POJO持久化以后就是PO
直接用它传递、传递过程中就是DTO
直接用来对应表示层就是VO
(六) DAO:
data access object数据访问对象
这个大家最熟悉,和上面几个O区别最大,基本没有互相转化的可能性和必要。
主要用来封装对数据库的访问。通过它可以把POJO持久化为PO,用PO组装出来VO、DTO
原生jdbc六步骤:
基础常识:
面向对象的特征有哪些方面?
- 抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
- 封装:通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。可以说,封装就是隐藏一切可隐藏的东西,只向外界提供最简单的编程接口(可以想想普通洗衣机和全自动洗衣机的差别,明显全自动洗衣机封装更好因此操作起来更简单;我们现在使用的智能手机也是封装得足够好的,因为几个按键就搞定了所有的事情)。
- 继承:继承是从已有类得到继承信息创建新类的过程。提供继承的类叫父类(超类、基类)、得到继承的类叫子类(派生类)。
- 多态性:多态性是指允许不同子类型的对象对同一消息作出不同的响应。简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。实现多态需要做两件事:1). 方法重写(子类继承父类并重写父类中的方法);2). 对象造型(用父类型引用引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)
简述面向对象的”六原则一法则”
- 单一职责原则:一个类只做它该做的事情。(单一职责原则想表达的就是”高内聚”,写代码最终极的原则只有六个字”高内聚、低耦合”,就如同葵花宝典或辟邪剑谱的中心思想就八个字”欲练此功必先自宫”,所谓的高内聚就是一个代码模块只完成一项功能,在面向对象中,如果只让一个类完成它该做的事,而不涉及与它无关的领域就是践行了高内聚的原则,这个类就只有单一职责。我们都知道一句话叫”因为专注,所以专业”,一个对象如果承担太多的职责,那么注定它什么都做不好。这个世界上任何好的东西都有两个特征,一个是功能单一,好的相机绝对不是电视购物里面卖的那种一个机器有一百多种功能的,它基本上只能照相;另一个是模块化,好的自行车是组装车,从减震叉、刹车到变速器,所有的部件都是可以拆卸和重新组装的,好的乒乓球拍也不是成品拍,一定是底板和胶皮可以拆分和自行组装的,一个好的软件系统,它里面的每个功能模块也应该是可以轻易的拿到其他系统中使用的,这样才能实现软件复用的目标。)
- 开闭原则:软件实体应当对扩展开放,对修改关闭。(在理想的状态下,当我们需要为一个软件系统增加新功能时,只需要从原来的系统派生出一些新类就可以,不需要修改原来的任何一行代码。要做到开闭有两个要点:①抽象是关键,一个系统中如果没有抽象类或接口系统就没有扩展点;②封装可变性,将系统中的各种可变因素封装到一个继承结构中,如果多个可变因素混杂在一起,系统将变得复杂
- 依赖倒转原则:面向接口编程。
- 里氏替换原则:任何时候都可以用子类型替换掉父类型。(关于里氏替换原则的描述,Barbara Liskov女士的描述比这个要复杂得多,但简单的说就是能用父类型的地方就一定能使用子类型。里氏替换原则可以检查继承关系是否合理,如果一个继承关系违背了里氏替换原则,那么这个继承关系一定是错误的,需要对代码进行重构。例如让猫继承狗,或者狗继承猫,又或者让正方形继承长方形都是错误的继承关系,因为你很容易找到违反里氏替换原则的场景。需要注意的是:子类一定是增加父类的能力而不是减少父类的能力,因为子类比父类的能力更多,把能力多的对象当成能力少的对象来用当然没有任何问题。)
- 接口隔离原则:接口要小而专,绝不能大而全。(臃肿的接口是对接口的污染,既然接口表示能力,那么一个接口只应该描述一种能力,接口也应该是高度内聚的。例如,琴棋书画就应该分别设计为四个接口,而不应设计成一个接口中的四个方法,因为如果设计成一个接口中的四个方法,那么这个接口很难用,毕竟琴棋书画四样都精通的人还是少数,而如果设计成四个接口,会几项就实现几个接口,这样的话每个接口被复用的可能性是很高的。Java中的接口代表能力、代表约定、代表角色,能否正确的使用接口一定是编程水平高低的重要标识。)
- 合成聚合复用原则:优先使用聚合或合成关系复用代码。
- 迪米特法则:迪米特法则又叫最少知识原则,一个对象应当对其他对象有尽可能少的了解。(迪米特法则简单的说就是如何做到”低耦合”,门面模式和调停者模式就是对迪米特法则的践行。对于门面模式可以举一个简单的例子,你去一家公司洽谈业务,你不需要了解这个公司内部是如何运作的,你甚至可以对这个公司一无所知,去的时候只需要找到公司入口处的前台美女,告诉她们你要做什么,她们会找到合适的人跟你接洽,前台的美女就是公司这个系统的门面。再复杂的系统都可以为用户提供一个简单的门面,Java Web开发中作为前端控制器的Servlet或Filter不就是一个门面吗,浏览器对服务器的运作方式一无所知,但是通过前端控制器就能够根据你的请求得到相应的服务。调停者模式也可以举一个简单的例子来说明,例如一台计算机,CPU、内存、硬盘、显卡、声卡各种设备需要相互配合才能很好的工作,但是如果这些东西都直接连接到一起,计算机的布线将异常复杂,在这种情况下,主板作为一个调停者的身份出现,它将各个设备连接在一起而不需要每个设备之间直接交换数据,这样就减小了系统的耦合度和复杂度,如下图所示。迪米特法则用通俗的话来将就是不要和陌生人打交道,如果真的需要,找一个自己的朋友,让他替你和陌生人打交道。)
Java语言具有哪些特点?
1. Java为纯面向对象的语言。它能够直接反应现实生活中的对象。
2. 具有平台无关性。java利用Java虚拟机运行字节码,无论是在Windows、Linux还是MacOS等其它平台对Java程序进行编译,编译后的程序可在其它平台运行。
3. Java为解释型语言,编译器把Java代码编译成平台无关的中间代码,然后在JVM上解释运行,具有很好的可移植性。
4. Java提供了很多内置类库。如对多线程支持,对网络通信支持,最重要的一点是提供了垃圾回收器。
5. Java具有较好的安全性和健壮性。Java提供了异常处理和垃圾回收机制,去除了C++中难以理解的指针特性。
6. Java语言提供了对Web应用开发的支持。
简述java的多态
Java多态可以分为编译时多态和运行时多态。
编译时多态:
主要指方法的重载,即通过参数列表的不同来区分不同的方法。运行时多态主要指继承父类和实现接口时,可使用父类引用指向子类对象。
运行时多态:
的实现主要依靠方法表,方法表中最先存放的是Object类的方法,接下来是该类的父类的方法,最后是该类本身的方法。如果子类改写了父类的方法,那么子类和父类的那些同名方法共享一个方法表项,都被认作是父类的方法。因此可以实现运行时多态。
Java提供的多态机制
Java提供了两种用于多态的机制,分别是重载与覆盖。
1. 重载:重载是指同一个类中有多个同名的方法,但这些方法有不同的参数,在编译期间就可以确定调用哪个方法。
2. 覆盖:覆盖是指派生类重写基类的方法,使用基类指向其子类的实例对象,或接口的引用变量指向其实现类的实例对象,在程序调用的运行期根据引用变量所指的具体实例对象调用正在运行的那个对象的方法,即需要到运行期才能确定调用哪个方法。
Java为什么不支持多继承(可多重继承)?
为了程序的结构能够更加清晰从而便于维护。假设Java语言支持多重继承,类C继承自类A和类B,如果类A和B都有自定义的成员方法f(),那么当代码中调用类C的f()会产生二义性。
Java语言通过实现多个接口间接支持多重继承,接口由于只包含方法定义,不能有方法的实现,类C继承接口A与接口B时即使它们都有方法f(),也不能直接调用方法,需实现具体的f()方法才能调用,不会产生二义性。
多重继承会使类型转换、构造方法的调用顺序变得复杂,会影响到性能。
重点类:
简述Object类常用方法
1. hashCode:通过对象计算出的散列码。用于map型或equals方法。
需要保证同一个对象多次调用该方法,总返回相同的整型值。
2. equals:判断两个对象是否一致。需保证equals方法相同对应的对象hashCode也相同。
3. toString: 用字符串表示该对象
4. clone:深拷贝一个对象
简述内部类及其作用
成员内部类:作为成员对象的内部类。可以访问private及以上外部类的属性和方法。外部类想要访问内部类属性或方法时,必须要创建一个内部类对象,然后通过该对象访问内部类的属性或方法。
外部类也可访问private修饰的内部类属性。
局部内部类:存在于方法中的内部类。访问权限类似局部变量,只能访问外部类的final变量。
匿名内部类:只能使用一次,没有类名,只能访问外部类的final变量。
静态内部类:类似类的静态成员变量。
静态内部类:
静态内部类是指使用 static 修饰的内部类。示例代码如下:
public class Outer {
static class Inner {
// 静态内部类
}
}
上述示例中的 Inner 类就是静态内部类。静态内部类有如下特点。
1)在创建静态内部类的实例时,不需要创建外部类的实例。
public class Outer {
static class Inner {
}
}
class OtherClass {
Outer.Inner oi = new Outer.Inner();
}
2)静态内部类中可以定义静态成员和实例成员。外部类以外的其他类需要通过完整的类名访问静态内部类中的静态成员,如果要访问静态内部类中的实例成员,则需要通过静态内部类的实例。
public class Outer {
static class Inner {
int a = 0; // 实例变量a
static int b = 0; // 静态变量 b
}
}
class OtherClass {
Outer.Inner oi = new Outer.Inner();
int a2 = oi.a; // 访问实例成员
int b2 = Outer.Inner.b; // 访问静态成员
}
3)静态内部类可以直接访问外部类的静态成员,如果要访问外部类的实例成员,则需要通过外部类的实例去访问。
public class Outer {
int a = 0; // 实例变量
static int b = 0; // 静态变量
static class Inner {
Outer o = new Outer;
int a2 = o.a; // 访问实例变量
int b2 = b; // 访问静态变量
}
}
区别:
JDK与JRE区别
1. JDK:Java开发工具包(Java Development Kit),提供了Java的开发环境和运行环境。
2. JRE:Java运行环境(Java Runtime Environment),提供了Java运行所需的环境。
JDK包含了JRE。如果只运行Java程序,安装JRE即可。要编写Java程序需安装JDK.
面向过程和面向对象区别
面向过程(Procedure Oriented 简称PO :如C语言):
从名字可以看出它是注重过程的。当解决一个问题的时候,面向过程会把事情拆分成: 一个个函数和数据(用于方法的参数) 。然后按照一定的顺序,执行完这些方法(每个方法看作一个过程),等方法执行完了,事情就搞定了。
面向对象(Object Oriented简称OO :如C++,JAVA等语言):
看名字它是注重对象的。当解决一个问题的时候,面向对象会把事物抽象成对象的概念,就是说这个问题里面有哪些对象,然后给对象赋一些属性和方法,然后让每个对象去执行自己的方法,问题得到解决。
面向过程
优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、 Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
缺点:没有面向对象易维护、易复用、易扩展
面向对象
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护
缺点:性能比面向过程低。
int和Integer的区别
1、Integer是int的包装类,int则是java的一种基本数据类型
2、Integer变量必须实例化后才能使用,而int变量不需要
3、Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值
4、Integer的默认值是null,int的默认值是0
延伸:
关于Integer和int的比较
1、由于Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相等的(因为new生成的是两个对象,其内存地址不同)。
Integer i = new Integer(100);
Integer j = new Integer(100);
System.out.print(i == j); //false
2、Integer变量和int变量比较时,只要两个变量的值是向等的,则结果为true(因为包装类Integer和基本数据类型int比较时,java会自动拆包装为int,然后进行比较,实际上就变为两个int变量的比较)
Integer i = new Integer(100);
int j = 100;
System.out.print(i == j); //true
3、非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。(因为 ①当变量值在-128~127之间时,非new生成的Integer变量指向的是java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同;②当变量值在-128~127之间时,非new生成Integer变量时,java API中最终会按照new Integer(i)进行处理(参考下面第4条),最终两个Interger的地址同样是不相同的)
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 j = 100;
System.out.print(i == j); //true
Integer i = 128;
Integer j = 128;
System.out.print(i == j); //false
对于第4条的原因:
java在编译Integer i = 100 ;时,会翻译成为Integer i = Integer.valueOf(100);,而java API中对Integer类型的valueOf的定义如下:
public static Integer valueOf(int i){
assert IntegerCache.high >= 127;
if (i >= IntegerCache.low && i <= IntegerCache.high){
return IntegerCache.cache[i + (-IntegerCache.low)];
}
return new Integer(i);
}
java对于-128到127之间的数,会进行缓存,Integer i = 127时,会将127进行缓存,下次再写Integer j = 127时,就会直接从缓存中取,就不会new了。
如果有错误的地方,还请指正。
重载与重写的区别
1. 重写是父类与子类之间的关系,是垂直关系;重载是同一类中方法之间的关系,是水平关系。
2. 重写只能由一个方法或一对方法产生关系;重载是多个方法之间的关系。
3. 重写要求参数列表相同;重载要求参数列表不同。
4. 重写中,调用方法体是根据对象的类型来决定的,而重载是根据调用时实参表与形参表来对应选择方法体。
5. 重载方法可以改变返回值的类型,覆盖方法不能改变返回值的类型。
判等运算符==与equals的区别
== 比较的是引用,equals比较的是内容。
1. 如果变量是基础数据类型,== 用于比较其对应值是否相等。如果变量指向的是对象,== 用于比较两个对象是否指向同一块存储空间。
2. equals是Object类提供的方法之一,每个Java类都继承自Object类,所以每个对象都具有equals这个方法。Object类中定义的equals方法内部是直接调用 == 比较对象的。但通过重写equals方法可以让它不是比较引用而是比较数据内容。
一般都会重写equals方法,重写equals方法也要重写 hashCode方法。
重写hashCode方法需保证相同对象的hashCode值也相同。
抽象类与接口的相同点和区别
说明:
接口可以继承接口,而且支持多重继承。
抽象类可以实现(implements)接口,抽象类可继承具体类也可以继承抽象类。
相同点:
- 都不能被实例化。
区别:
- 实现:
- 抽象类的子类使用 extends 来继承;接口必须使用 implements 来实现接口。
- 抽象类可以有抽象方法和具体方法,接口全为抽象方法。
- 抽象类有方法的定义与实现;接口只能有方法定义,不能有方法的实现。
- 构造函数:
- 抽象类可以有构造函数;接口不能有。
- main 方法:
- 抽象类可以有 main 方法,并且我们能运行它;接口不能有 main 方法。
- 实现数量:
- 类可以实现很多个接口;但是只能继承一个抽象类。
- 访问修饰符:
- 接口中的方法默认使用 public 修饰;抽象类中的方法可以是任意访问修饰符。
有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法。
抽象类中可以定义成员变量,而接口中定义的成员变量实际上都是常量。
2、你选择使用接口和抽象类的依据是什么?
接口和抽象类的概念不一样。接口是对动作的抽象,抽象类是对根源的抽象。
抽象类表示的是,这个对象是什么。接口表示的是,这个对象能做什么。比如,男人,女人,这两个类(如果是类的话……),他们的抽象类是人。说明,他们都是人。
人可以吃东西,狗也可以吃东西,你可以把“吃东西”定义成一个接口,然后让这些类去实现它.所以,在高级语言上,一个类只能继承一个类(抽象类)(正如人不可能同时是生物和非生物),但是可以实现多个接口(吃饭接口、走路接口)。
1、抽象类和接口都不能直接实例化,如果要实例化,抽象类变量必须指向实现所有抽象方法的子类对象,接口变量必须指向实现所有接口方法的类对象。
2、抽象类要被子类继承,接口要被类实现。
3、接口只能做方法申明,抽象类中可以做方法申明,也可以做方法实现
4、接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量。
5、抽象类里的抽象方法必须全部被子类所实现,如果子类不能全部实现父类抽象方法,那么该子类只能是抽象类。同样,一个实现接口的时候,如不能全部实现接口方法,那么该类也只能为抽象类。
6、抽象方法只能申明,不能实现,接口是设计的结果 ,抽象类是重构的结果
7、抽象类里可以没有抽象方法
8、如果一个类里有抽象方法,那么这个类只能是抽象类
9、抽象方法要被实现,所以不能是静态的,也不能是私有的。
10、接口可继承接口,并可多继承接口,但类只能单根继承。
Java和JavaSciprt的区别
java是静态语言,js是动态语言
- 基于对象和面向对象:Java是一种真正的面向对象的语言,即使是开发简单的程序,必须设计对象;JavaScript是种脚本语言,它可以用来制作与网络无关的,与用户交互作用的复杂软件。它是一种基于对象(Object-Based)和事件驱动(Event-Driven)的编程语言,因而它本身提供了非常丰富的内部对象供设计人员使用。
- 解释和编译:Java的源代码在执行之前,必须经过编译。JavaScript是一种解释性编程语言,其源代码不需经过编译,由浏览器解释执行。(目前的浏览器几乎都使用了JIT(即时编译)技术来提升JavaScript的运行效率)
- 强类型变量和类型弱变量:Java采用强类型变量检查,即所有变量在编译之前必须作声明;JavaScript中变量是弱类型的,甚至在使用变量前可以不作声明,JavaScript的解释器在运行时检查推断其数据类型。
- 代码格式不一样。
fail-fast和fail-safe迭代器的区别
1. fail-fast直接在容器上进行,在遍历过程中,一旦发现容器中的数据被修改,就会立刻抛出ConcurrentModificationException异常从而导致遍历失败。常见的使用fail-fast方式的容器有HashMap和ArrayList等。
2. fail-safe这种遍历基于容器的一个克隆。因此对容器中的内容修改不影响遍历。常见的使用fail-safe方式遍历的容器有ConcurrentHashMap(线程安全的hashmap)和CopyOnWriteArrayList。
成员变量与局部变量的区别
1、在类中的位置不同
成员变量:在类中方法外面
局部变量:在方法或者代码块中,或者方法的声明上(即在参数列表中)
2、在内存中的位置不同,可以看看Java程序内存的简单分析
成员变量:在堆中(方法区中的静态区)
局部变量:在栈中
3、生命周期不同
成员变量:随着对象的创建而存在,随着对象的消失而消失
局部变量:随着方法的调用或者代码块的执行而存在,随着方法的调用完毕或者代码块的执行完毕而消失
4、初始值
成员变量:有默认初始值
局部变量:没有默认初始值,使用之前需要赋值,否则编译器会报错(The local variable xxx may not have been initialized)
String、StringBuffer、StringBuilder区别
操作字符串的类有:String、StringBuffer、StringBuilder。
String 和 StringBuffer、StringBuilder 的区别:
String 声明的是不可变的对象,每次操作都会生成新的 String 对象,然后将指针指向新的 String 对象,而 StringBuffer、StringBuilder 可以在原有对象的基础上进行操作,所以在经常改变字符串内容的情况下最好不要使用 String。
StringBuffer 和 StringBuilder 最大的区别在于,StringBuffer 是线程安全的,而 StringBuilder 是非线程安全的,但 StringBuilder 的性能却高于 StringBuffer,所以在单线程环境下推荐使用 StringBuilder,多线程环境下推荐使用 StringBuffer。
- 基本原则:如果要操作少量的数据,用String ;
- 单线程操作大量数据,用StringBuilder ;
- 多线程操作大量数据,用StringBuffer。
String类采用利用final修饰的字符数组进行字符串保存,因此不可变。如果对String类型对象修改,需要新建对象,将老字符和新增加的字符一并存进去。
StringBuilder采用无final修饰的字符数组进行保存,因此可变。但线程不安全。
StringBuffer采用无final修饰的字符数组进行保存,可理解为实现线程安全的StringBuilder。
final、finally、finalize的区别
1. final用于声明属性、方法和类,分别表示属性不可变、方法不可覆盖、类不可继承。
2. finally作为异常处理的一部分,只能在try/catch语句中使用,finally附带一个语句块用来表示这个语句最终一定被执行,经常被用在需要释放资源的情况下。
3. finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的finalize()方法。当垃圾回收器准备好释放对象占用空间时,首先会调用finalize()方法,并在下一次垃圾回收动作发生时真正回收对象占用的内存。
throw与throws的区别
throw一般是用在方法体的内部,由开发者定义当程序语句出现问题后主动抛出一个异常。
throws一般用于方法声明上,代表该方法可能会抛出的异常列表。
数据类型问题:
为什么要把String设计为final不变量(不可被继承)?
1. 节省空间:字符串常量存储在JVM的字符串池中可以被用户共享。
2. 提高效率:String会被不同线程共享,是线程安全的。在涉及多线程操作中不需要同步操作。
3. 安全:String常被用于用户名、密码、文件名等使用,由于其不可变,可避免黑客行为对其恶意修改。
如何实现字符串的反转及替换?
答:方法很多,可以自己写实现也可以使用String或StringBuffer/StringBuilder中的方法。有一道很常见的面试题是用递归实现字符串反转。
Java中一维数组和二维数组的声明方式?
一维数组的声明方式:
1. type arrayName[]
2. type[] arrayName
二维数组的声明方式:
1. type arrayName[][]
2. type[][] arrayName
3. type[] arrayName[]
其中type为基本数据类型或类,arrayName为数组名字
String s = new String(“xyz”);创建了几个字符串对象?
答:两个对象,一个是静态区的”xyz”,一个是用new创建在堆上的对象。
用最有效率的方法计算2乘以8
2 << 3(左移3位相当于乘以2的3次方,右移3位相当于除以2的3次方)。
Math.round方法
Math.round(11.5)的返回值是12,Math.round(-11.5)的返回值是-11。四舍五入的原理是在参数上加0.5然后进行下取整。
short s1 = 1; s1 += 1;有错吗?
对于short s1 = 1; s1 = s1 + 1;由于1是int类型,因此s1+1运算结果也是int 型,需要强制转换类型才能赋值给short型。而short s1 = 1; s1 += 1;可以正确编译,因为s1+= 1;相当于s1 = (short)(s1 + 1);其中有隐含的强制类型转换。
float f=3.4;是否正确
不正确。3.4是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4; 或者写成float f =3.4F;。
字节序定义以及Java属于哪种字节序?
字节序是指多字节数据在计算机内存中存储或网络传输时个字节的存储顺序。通常由小端和大端两组方式。
1. 小端:低位字节存放在内存的低地址端,高位字节存放在内存的高地址端。
2. 大端:高位字节存放在内存的低地址端,低位字节存放在内存的高地址端。
Java语言的字节序是大端。
代码块相关问题
代码块执行顺序
先后顺序:静态成员变量、成员变量、构造方法。
详细的先后顺序:父类静态变量、父类静态代码块、子类静态变量、子类静态代码块、父类非静态变 量、父类非静态代码块、父类构造函数、子类非静态变量、子类非静态代码块、子类构造函数。
1、代码块分类:
(1)静态代码块:
①Java静态代码块中的代码会在类加载JVM时运行,且只被执行一次
②静态块常用来执行类属性的初始化
③静态块优先于各种代码块以及构造函数,如果一个类中有多个静态代码块,会按照书写顺序依次执行
④静态代码块可以定义在类的任何地方中除了方法体中【这里的方法体是任何方法体】
⑤静态代码块不能访问普通变量
(2)构造代码块:
①构造代码块在创建对象时被调用,每次创建对象都会调用一次
②构造代码块优先于构造函数执行,同时构造代码块的运行依赖于构造函数
③构造代码块在类中定义
(3)普通代码块:
①普通代码块定义在方法体中
②普通代码块与构造代码块的格式一致都是{}
③普通代码块与构造代码块唯一能直接看出的区别是构造代码块是在类中定义的,而普通代码块是在方法体中定义的
2、代码块的执行顺序:
静态代码块——> 构造代码块 ——> 构造函数——> 普通代码块
3、继承中代码块执行顺序:
(1)父类静态块——>子类静态块——>父类构造代码块——>父类构造器
——>子类构造代码块——>子类构造器——>普通代码块
finally代码块是否一定会执行?
当遇到下面情况不会执行。
1. 当程序在进入try语句块之前就出现异常时会直接结束。
2. 当程序在try块中强制退出时,如使用System.exit(0),也不会执行finally块中的代码。
其它情况下,在try/catch/finally语句执行的时候,try块先执行,当有异常发生,catch和finally进行处理后程序就结束了,当没有异常发生,在执行完finally中的代码后,后面代码会继续执行。值得注意的是,当try/catch语句块中有return时,finally语句块中的代码会在return之前执行。如果try/catch/finally块中都有return语句,finally块中的return语句会覆盖try/catch模块中的return语句。
反射机制
Java反射机制是指在程序的运行过程中可以构造任意一个类的对象、获取任意一个类的成员变量和成员方法、获取任意一个对象所属的类信息、调用任意一个对象的属性和方法。反射机制使得Java具有动态获取程序信息和动态调用对象方法的能力。可以通过以下类调用反射API。
Class类:可获得类属性方法
Field类:获得类的成员变量
Method类:获取类的方法信息
Construct类:获取类的构造方法等信息
作用:可以操作类的字节码文件。
获取字节码的方式:
通过Class类中的静态方法forName();可以获取到字节码对象
Class class1 = class.forName("包名.类名")
直接用该类.class获取字节码文件对象
Class class2 = Dog.class
通过类的实例获取该类的字节码文件对象
Class class3 = new Dog().getClass();
序列化与反序列化
序列化是一种将对象转换成字节序列的过程,用于解决在对对象流进行读写操作时所引发的问题。序列化可以将对象的状态写在流里进行网络传输,或者保存到文件、数据库等系统里,并在需要的时候把该流读取出来重新构造成一个相同的对象。
序列化:将java对象转化为字节序列,由此可以通过网络对象进行传输。
反序列化:将字节序列转化为java对象。
具体实现:实现Serializable接口,或实现Externalizable接口中的writeExternal()与readExternal()方法。
举例说明同步和异步
如果系统中存在临界资源(资源数量少于竞争资源的线程数量的资源),例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就必须进行同步存取(数据库操作中的排他锁就是最好的例子)。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。
事实上,所谓的同步就是指阻塞式操作,而异步就是非阻塞式操作。
标识符:
如何使用标识符
标识符的组成:标识符由26个英文字母大小写,数字:0-9符号:_$组成。
在使用标识符的时候有四点规则需要你遵守:
1标识符可以由字母、数字、下划线(_)、美元符($)组成,但不能包含 @、%、空格等其它特殊字符,不能以数字开头。譬如:123name就是不合法滴。
2标识符不能是 Java关键字和保留字( Java预留的关键字,以后的升级版本中有可能作为关键字),但可以包含关键字和保留字。如:不可以使用 static作为标识符,但是 Mystatic可以。
3标识符是严格区分大小写的。 所以涅,一定要分清楚 educoder和 Educoder是两个不同的标识符哦!
4标识符的命名最好能反映出其作用,比如要定义用户名就可以使用 userName作为标识符。
简述Java基本数据类型(8个)
byte: 占用1个字节,取值范围-128 ~ 127
short: 占用2个字节,取值范围-215 ~ 215 -1
int:占用4个字节,取值范围-231 ~ 231 -1
long:占用8个字节
float:占用4个字节
double:占用8个字节
char: 占用2个字节
boolean:占用大小根据实现虚拟机不同有所差异
获取Class对象的方法:
1:类型.class,例如:String.class
2:对象.getClass(),例如:”hello”.getClass()
3:Class.forName(),例如:Class.forName(“java.lang.String”)
简述Java中Class对象
java中对象可以分为实例对象和Class对象,每一个类都有一个Class对象,其包含了与该类有关的信息。
简述自动装箱拆箱
对于Java基本数据类型,均对应一个包装类。
装箱就是自动将基本数据类型转换为包装器类型,如int->Integer
拆箱就是自动将包装器类型转换为基本数据类型,如Integer->int
构造器是否可被重写
构造器不能被继承,因此不能被重写,但可以被重载。
引用传递or值传递
问:当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?
答:是值传递。Java语言的方法调用只支持参数的值传递。
当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的属性可以在被调用过程中被改变,但对对象引用的改变是不会影响到调用者的。
为什么要同时重写equals方法和hashCode方法
一、重写equals方法和hashcode方法的目的
在做对象间的比较时,我们通常采用equals方法来判断它们是否相等,如果地址值不同,则俩个对象不等。那为什么我们还要重写这俩个方法,换句话说,重写这俩个方法的目的在哪。
首先,我们思考这样一个需求:当一个数据表中有A和B俩个人,只要他们的名字和身份证这俩个字段相等,那么我们就认定这俩个人是同一个人,不去考虑他们的地址值是否相等来比较这俩个人是不是同一个人。
再有,这样一个情形:我们用一个拼接好的hello world的字符串和一个new出来的hello world作比较,只要内容一样,则俩个变量相等,如果只用equals来比较的话,他们势必不相等。
所以,重写equals方法和hashcode方法的目的就是为了实现一些合乎情理,切实际,在现实生活中经常出现的一些情景,针对这些情景来提出一些需求,为了满足这个需求从而采取的措施。如果不重写的话,在他的源码中比较的是俩个对象的地址值。
二、为什么要同时重写equals方法和hashcode方法
了解目的之后,所以我们需要重写这俩个方法,那只重写其中的一个方法可行嘛?我们来分析一下:
只重写equals方法:这种方法是可行的,但是假如有这样一个情况,在做俩个对象A和B的比较时,B对象是存放在集合里的,且集合里的数据多达几万条,那我们还要调用equals方法来一个一个比较嘛,当然可以,但是这样的话,效率就必然成了一个问题,所以,这个时候,就需要我们的hashcode方法了。
只重写hashcode方法呢:在做对象之间的比较时,我们要知道:
如果equals方法相等,则他们hsahcode的hash值必然相等,俩个对象必然相等
如果equals方法不等,则他们hashcode的hash值不一定不等,俩个对象必然不等
如果hashcode的hash值不等,则俩个对象必然不等
如果hashcode的hash值相等,俩个对象不一定相等,要在判断equals是否相等来比较俩个对象是否相等。
所以,需要同时重写equals方法和hashcode方法。因为要保证在实现hash表的扩展性。
三、重写equals方法和hashcode方法的作用
其实,如果看懂了上文的话,这里也就知道了重写这俩个方法的作用,
第一点:就是为了更加高效的去比较俩个对象是否相等。
第二点:实现特殊的需求。
四、如何重写equals方法和hashcode方法
在重写的过程中,我们要遵守equals方法和hashcode方法的特性,也就是要一一证明它的特性。
1.equals方法的特性:
(1)自反性:对于任何非空引用x,x.equals(x)应该返回true;
(2)对称性:对于任何引用x,和y,当且仅当,y.equals(x)返回true,x.equals(y)也应该返回true;
(3)传递性:对于任何引用x,y,z,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也应该返回true;
(4)一致性:如果x,y引用的对象没有发生变化,反复调用x.equals(y)应该返回同样的结果;
(5)对于任意非空引用x,x.equals(null)返回false;
2.而hashcode方法的特性:
(1)HashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,HashCode经常用于确定对象的存储地址;
(2)如果两个对象相同, equals方法一定返回true,并且这两个对象的HashCode一定相同;
(3)两个对象的HashCode相同,并不一定表示两个对象就相同,即equals()不一定为true,只能够说明这两个对象在一个散列存储结构中。
(4)如果对象的equals方法被重写,那么对象的HashCode也尽量重写,以保证equals方法相等时两个对象hashcode返回相同的值(eg:Set集合中确保自定义类的成功去重)。
3.如何重写equals方法和hashcode方法
自己摸索,哈哈哈。
public class User {
private String id;
private String name;
private String age;
public User(){
}
public User(String id, String name, String age){
this.id = id;
this.name = name;
this.age = age;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
@Override
public String toString() {
return this.id + " " + this.name + " " + this.age;
}
@Override
public boolean equals(Object obj) {
if(this == obj){
return true;//地址相等
}
if(obj == null){
return false;//非空性:对于任意非空引用x,x.equals(null)应该返回false。
}
if(obj instanceof User){
User other = (User) obj;
//需要比较的字段相等,则这两个对象相等
if(equalsStr(this.name, other.name)
&& equalsStr(this.age, other.age)){
return true;
}
}
return false;
}
private boolean equalsStr(String str1, String str2){
if(StringUtils.isEmpty(str1) && StringUtils.isEmpty(str2)){
return true;
}
if(!StringUtils.isEmpty(str1) && str1.equals(str2)){
return true;
}
return false;
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (name == null ? 0 : name.hashCode());
result = 31 * result + (age == null ? 0 : age.hashCode());
return result;
}
}