Java部分转载自:blog.csdn.net/weixin_4673… 后续部分由本人慢慢更新
1 Android逆向介绍
由于移动互联网的快速发展以及安卓手机的普及,移动应用的安全性也越来越被人关注,不管是安卓还是苹果每年都会被报出许多安全相关问题,其中属安卓端的安全对抗最为激烈,提到安全对抗那么首先需要了解的就是逆向,逆向是一切攻击行为的基础,攻击者一般是拿不到程序源码的,所以只有通过一些逆向手段来分析目标逻辑,从而实现进一步的攻击,下面来分析一下关于Android逆向的应用领域。
首先最常见的就是安卓软件破解,包括同行间的竞品分析,黑灰产的游戏内购,外挂,盗版仿冒,VIP功能的破解,以及协议分析后的用户数据爬取,薅羊毛等。其次是对安卓漏洞的挖掘及利用,主要分为应用层漏洞、系统层漏洞以及内核层漏洞。再有就是近两年监管单位比较关注的安卓应用的隐私合规审计,最后是对恶意软件的分析,包括安卓病毒的研判查杀,隐私窃取勒索病毒或钓鱼欺诈软件的分析溯源等。
Android逆向知识体系
对安卓逆向的应用场景有个基本了解后再来看下安卓逆向需要学习哪些知识
首先是开发相关的知识,包括以下知识点:
- 安卓开发语言:Java、C/C++、Smali、ARM/ARM64、Python、
- 安卓开发工具:Android Studio的使用、Visual Studio Code的使用
- 源码分析工具:SourceInsight工具的使用、
- 安卓系统源码、内核源码的编译与修改、设备刷机、
- 安全开发知识:代码混淆技术、应用加固技术、动态加载与插件化技术、
然后是逆向相关的一些必备知识点:
- 常见分析工具的使用:IDA Pro、GDB、JEB、Jadx、GDA等、
- APK中文件格式的熟悉:APK、DEX、ELF、
- 静态分析方法与动态调试技巧、
- 注入原理、Hook原理以及常见hook框架、
- 常见反调试原理及其绕过、模拟器检测及其绕过、
- 基础密码学、网络基础、协议分析技术、抓包技术、
Android逆向常用工具介绍
最后看下逆向工作中经常会用到的一些工具,这里简单介绍下每个工具的用途,后续会详细对每个工具进行使用。
APK反编译工具:
-
AndroidManifest.xml清单文件解析:AXMLPrinter2
- 该工具主要用于对APK中的清单文件进行解析,有助于我们了解目标apk的组件信息。
-
APK反编译工具:baksmali、smali、dex2jar、apktool
- baksmali:该工具用来将一个DEX文件反编译为Smali汇编文件
- smali:该工具用来将Smali汇编文件回编译为Dex文件
- dex2jar:该工具用于将dex文件反编译为jar文件
- apktool:该工具支持将apk反编译为Smali汇编文件,也可以将Smali汇编回编译为apk
-
APK签名工具:signapk
- signapk:用于对回编译后的apk文件进行签名,没有签名apk文件安装到到模拟器或手机中,会报错
-
集成工具:AndroidKiller
- AndroidKiller:一个将上面几种工具集合在一起的工具
静态/动态分析工具:IDA Pro、GDB、JEB、Jadx、GDA、
Hook工具:Xposed框架、Frida框架、
文件格式分析/文件对比工具:010Editor、BCompare、
安卓源码分析工具:SourceInsight、understand、
协议分析抓包工具:Burpsuite、Fiddler、WireShark、
安卓设备:模拟器、实体机、
Android应用简单介绍
Android应用一般都是通过Java语言编写的,传统的Java应用是通过JVM虚拟机将编译成Java字节码的class文件运行起来的,虽然Android也是使用Java开发的,但最终执行的却不是Java字节码,因为谷歌开发了一个叫Dalvik的虚拟机,Android应用都是通过这个Dalvik虚拟机解释执行的。
Android开发环境搭建
安卓开发环境主要有以下几个环境需要配置
- Java SE Development Kit(JDK)
- Android SDK:编译,调试Android程序
- 集成开发程序IDE:Android Studio,IntellijIDEA , Eclipse ADT
- Android 设备:模拟器(AVD Manager,夜神,BlueStacks)
JDK环境配置
JDK(Java Development Kit)Java语言的软件开发工具包
两个主要工具:
- javac编译器,用于将源程序转成Java字节码
- java用来运行编译后的java程序
下载地址:JDK
下载安装完成后需要将Java安装路径设置到系统环境变量中
环境变量配置:系统不同设置的方法也不同,这里以win10举例。
点击新建按钮后会弹出一个新建系统变量的框,分别输入变量名:JAVA_HOME,和变量值:你的Java安装路径。
之后再以同样的方式添加一个变量名为:CLASSPATH,变量值为:.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar 的变量。
上面两个变量添加完成之后再找到系统变量名为Path的变量,双击进行编辑,新建两个环境变量
%JAVA_HOME%\bin
%JAVA_HOME%\jre\bin
点击确定,环境变量添加完事儿后,开启一个命令行验证一下是否生效。
λ java -version
java version "1.8.0_91"
Java(TM) SE Runtime Environment (build 1.8.0_91-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.91-b15, mixed mode)
λ javac -version
javac 1.8.0_91
输入以上两条命令都返回正常,那么说明安装成功。
Android SDK
Android SDK(软件开发包)下载
- 下载地址:Android SDK
下载好之后看下SDK中包含的文件
在日常开发中经常用到的工具有以下几个
AVD Manager.exe:该工具用于管理安卓的原生模拟器。
SDK Manager.exe:该工具用于对SDK进行管理。
monitor.exe:安卓设备监视器,又名ddms(Dalvik Debug Monitor Server)
路径:SDK\tools\monitor.bat
adb.exe:安卓调试桥,用于将PC与Android设备进行连接
路径:SDK\platform-tools\adb.exe
adb常见命令:
- 获取设备列表和设备状态: adb devices
- 安装Apk: adb install xxx.apk
- 卸载Apk: adb uninstall xxx.apk
- 列出当前安装的包名: adb shell pm list packages
- 清除缓存: adb shell pm clear 包名
- 强制停止应用: adb shell am force-stop
- 以debug的方式启动apk: adb shell am start -D -n 包名/Activity名
- 获取正在运行的顶层应用: adb shell dumpsys window | findstr mCurrentFocus
- 获取模拟器中的文件: adb pull
- 向模拟器中写文件: adb push
- 重启设备: adb reboot
- 查看日志: adb logcat
- adb shell: 进入模拟器或手机,操作其中的目录或文件,进入shell之后,使用的就是Linux系统下的shell命令,但android系统对其做了阉割
adb 调试桥-常见问题:由于adb是移动设备与PC连接的桥梁,所以很多手机助手,安全卫士都会默认开启adb连接移动设备,当我们使用adb发起调试的时候可能会造成一定的冲突,出现异常。
解决方法:重启adb
adb kill-server
adb start-server
可以看到android SDK给我们提供了许多开发调试Android程序的工具,逆向过程中也经常用到,可以添加个全局变量:
添加方法和之前添加JDK的方法一样,在系统变量Path中将android SDK目录下的tools目录和platform-tools目录的绝对路径添加进去即可。
Android Studio
Android开发的集成开发环境有Android Studio,Eclipse ADT。由于Eclipse工具已经比较旧了,现在一般都用Android Studio进行开发,并且也是官方提供更新的IDE,所以我们主要学习下Android Studio的使用。
下载地址:Android Studio
下载安装完成后首次启动会要求配置Android SDK,根据提示进行设置即可。
Android Studio的一些配置
取消打开最后的项目:Settings -> Appearance & Behavior -> System Settings下
UI样式的设置:Settings -> Appearance & Behavior -> Appearance
字体设置,常用程序员字体Consolas:Editor -> font
显示代码行数:Editor -> General -> Appearance
Android Studio的一些快捷键
alt+insert:快速创建各种方法,包括构造、重载等
alt+enter:快速导包
ctrl+/:快速单行注释
ctrl+shift+/:快速块注释
Android 设备
Android开发常见的模拟器,用的比较多的有以下两款,
原生模拟器:主要用来做开发使用,使用SDK工具中的AVD Manager.exe即可创建模拟器。
夜神模拟器:破解应用用的比较多,因为该模拟器支持一些别的,比如root,改IMEI号等功能。
下载地址:夜神模拟器
为了方便在PC端通过adb连接模拟器,还需要做一下端口转发:
- 夜神模拟器:adb connect 127.0.0.1:62001
- 逍遥模拟器:adb connect 127.0.0.1:21503
- 木木模拟器:adb connect 127.0.0.1:7555
做完端口转发后就能直接在PC端通过adb连接连上模拟器了
模拟器在日常工作中用的还是比较少的,一般在需要访问公司内网,但实体机没权限权限时会用到,这时在办公电脑上用模拟器就可以解决这个问题。
如果想自己编译安卓系统的话就需要用到实体机,所以最好再备一台可以root的实体机,一般实体机选择Google的亲儿子nexus系列或pixel系列都行,不过建议pixel系列,因为nexus比较老了嘛,好些应用在这些手机上跑起来都比较卡。
一、初识Java与Java程序剖析
1、JDK环境配置
关于JDK环境的配置,前面文章有提到过Android逆向-002.Android开发环境搭建
2、IntelliJ IDEA集成开发环境
IntelliJ IDEA是Java编程语言开发的集成环境。
下载:IntelliJ IDEA
3、Java程序剖析(从HelloWorld说起)
应用程序的主类:一个Java程序的源文件必须包含一个public static void main(String args[])方法,这个方法被称为应用程序的入口方法,args[]是main方法的参数,是一个字符串类型的数组,用来存储输入的参数。
源文件的命名:源文件名字必须与类的名字相同,扩展为.java,注意:Java区分大小写。
Java一般命名规则:
- 包的命名:全部小写(com.xxx.xx)
- 类的命名:单词首字母大写,如果由多个单词组成则每个单词首字母大写。(HelloWorld)
- 变量/方法的命名:首字母小写,如果由多个单词组成,则除第一个单词外,其余每个单词首字母大写。(getName)
- 常量的命名:全部大写,常加入下划线(MAX_VALUE)
所有命名规则必须遵循以下规则:
- 1.名称只能由字母、数字、下划线、$符号组成。
- 2.不能以数字开头
- 3.名称不能使用Java的关键字。
举例:将以下代码保存为HelloWorld.java
public class HelloWorld{
public static void main(String args[]){
int outValue = 1;
System.out.println("hello world: "+outValue);
}
}
编译:
javac.exe HelloWorld.java
生成:
ls
HelloWorld.class HelloWorld.java
运行:
java HelloWorld
hello world: 1
Java相对于C/C++的优点:支持跨平台、Java有内置的垃圾回收机制、更安全。
Java的跨平台运行机制:
Java语言的源程序->编译成Java字节码->通过JVM虚拟机进行解释执行从而实现跨平台运行
Java关键字:51个使用中,2个保留关键字(const、goto)所以共53个关键字。
访问控制:
- private 私有的、protected 受保护的、public 公开的
类,方法和变量修饰符:
- abstract 声明抽象类、class 类、extends 扩充,继承、final 终极,不可改变的、implements 实现、interface 接口、native 本地、new 创建、static 静态、strictfp 严格,精准、synchronized 线程同步、transient 短暂、volatile 易失
程序控制语句:
- break 跳出循环、continue 继续、return 返回、do 运行、while 循环、if 如果、else 反之、for 循环、instanceof 实例、switch 开关、case 开关的结果、default 默认
错误处理:
- try 捕获异常、catch 处理异常、finally 有没有异常都执行、throw 抛出一个异常对象、throws 声明一个异常可被抛出、assert 断言
包相关:
- import 引入、package 包
基本类型:
- boolean 布尔型、byte 字节型、char 字符型、double 双精度、float 浮点、int 整数、long 长整型、short 短整型、null 空、true 真、false 假、enum 枚举
变量引用:
- super 父类,超类、this 本类、void 无返回值
二、数据类型和基本操作
Java提供了两类数据类型,一类是基本数据类型(原始类型),一类是引用类型。
1、基本数据类型
Java语言中一共有8种基本数据类型,分别是:boolean、byte、short、int、long、float、double、char
分为四大类:
布尔类型:boolean(C/C++中的逻辑类型是bool)
- 常量:true、false
- 变量:使用关键字boolean来声明逻辑变量,声明时可以直接赋初始值。
整数类型:byte,short,int,long
- int:Java中的int型常量可以直接写值:比如120,int型变量需要使用关键字int来声明
- byte:Java中不存在byte类型的常量,但是可以将一定范围内的int型常量赋值给byte型变量,通过byte关键字可以来声明byte型变量
- short:和byte类型一样,Java中不存在short类型常量,但可以把一定范围内的int型常量赋值给short型变量。
字符类型:char
- char:用单引号括起来的unicode表中的字符
浮点类型:float、double
- float:内存分配4个字节的浮点数,占32位
- double:内存分配8个字节的浮点数,占64位
2、变量,常量以及赋值语句
变量:在程序运行过程中可以发生改变的
- 声明语法格式:数据类型 变量名 = 值;
常量:在程序运行过程中不能改变的,程序运行前就被初始化的。
- 常量的语法格式,只需要在变量的语法格式前面添加关键字final即可。常量名必须大写。
- final关键字用于声明常量,方法和类,含义是"这是无法改变的"或"最终状态"
- final 数据类型 常量名 = 值;举例:final double P=3.14;final int MIN_VALUE=0;
- final修饰的常量只能被赋值一次,赋值后值不可以再改变。
- final修饰的方法不能被子类方法重写,但是可以被继承。
- final修饰的类不能被继承,没有子类,final类中所有方法都是final的。
Java的赋值语句是"="号,举例int变量赋值:int a=1;表示声明一个int型变量a,并将1赋值给a。除了可以将数值常量赋值还支持将变量赋值给变量,举例:int b=a;表示声明一个int型变量并将变量a的值赋值给变量b。注意这里的变量a需要与变量b类型一致,类型不一致的变量赋值需要类型转换,别的类型赋值与此类似。
3、数据类型转换
基本类型大小:从小到大
- byte:字节型:占1字节
- boolean:布尔型:占1字节
- char:字符型:占1字节
- short:短整型:占2字节
- int:整型:占4字节
- float:单精度浮点型:占4字节
- long:长整型:占8字节
- double:双精度浮点型:占8字节
自动(隐私)类型转换:自动将小类型转换为大类型,不需要强制转换符
举例:int a = 5;double b = a;
此处将占4字节大小的int类型变量a的值赋值给了占8字节大小的double类型变量b,因为a是小类型转为大类型,所以int类型变量a的值会自动转为double类型并赋值给变量b。
强制类型转换:将大类型转换为小类型,需要通过强制转换符来实现强制转换。
举例:double a = 520.1314;int b = (int)a;
此处将占8字节大小的double类型变量a赋值给占4字节大小的int类型变量b,因为是大类型转为小类型,所以double型变量a需要先强制转为int类型变量才能赋值给int型变量b。
注意:强转可能产生精度丢失,例如double类型变量a值为520.1314,强转成int类型后数值将会变成520,强转导致小数位舍弃产生精度丢失。
4、引用数据类型
在Java中除去基本数据类型的其他类型都是引用类型,包括自定义的class类,引用类型指向一个对象,不是原始的值,指向对象的变量是引用变量。
引用类型常见的有:String、StringBuffer、ArrayList、HashSet、HashMap等。
5、枚举
定义枚举:
使用enum声明枚举类型:
enum名字{
常量列表
}
其中常量列表用逗号分隔的字符序列,称为枚举类型的常量。
示例:
enum Season{
spring,summer,autumn,winter
}
上面声明了名字为Season的枚举类型,该枚举类型有4个常量。
枚举变量:
声明了名字为Season的枚举类型,该枚举类型有4个常量。
声明了一个枚举类型后,就可以用该枚举类型声明一个枚举变量。
示例:
Season x;该枚举变量只能取值枚举类型中的常量。通过使用枚举名和"."运算符来获取枚举类型中的常量。例如:x = Season.spring
6、注释
Java支持三种注释方式:
- 单行注释:以双斜杠"//"标识,只能注释一行内容,用在注释信息较少的是偶。
- 多行注释:以"/*“标识开头,”*/"标识结尾的注释方式,能注释多行内容,
- 文档注释:以"/**“标识开头,”*/"标识结尾的注释方式,一般用在类、方法和变量上面,用来描述其作用。
三、Java运算符及优先级
1、算术运算符:
算符运算符:
- +、-、*、/、%、++、–
2、赋值运算符
赋值运算符:
- 格式:变量=表达式;
- 复合赋值运算法:+=、-=、*=、/=、%=
3、关系运算符
关系运算法:属于二目运算符,用来比较两个值的关系,结果是boolean型,当运算符对应的关系成立时,运算结果是true、否者false。
- >、<、>=、<=、==、!=
4、逻辑运算符
逻辑运算法:
- 逻辑与:&&
- 逻辑或:||
- 逻辑非:!
注意:其中&&、||属于短路运算、也就是如果第一个表达式能决定最终结果则不进行后面的计算。
5、位运算符
位运算法:两个整型数据实施位运算,即对两个整型数据对应的位进行运算得到一个新的整型数据
- 按位与:&
- 按位或:|
- 按位非:~
- 按位异或:^
6、其他运算法
instanceof运算符:instanceof运算符是一个二目运算符,左边的操作元是一个对象,右边是一个类,当左边的对象和右边的类或子类创建的对象时,该运算符运算的结果是true。
7、运算符优先级
Java中的运算符优先级从高到低排序(实际工作用的较少,运算顺序主要用括号控制)
- 圆括号:()
- 逻辑非、自增、自减:!、++、–
- 乘法、除法、取余:*、/、%
- 加法、减法:+、-
- 小于、小于等于、大于、大于等于:<、<=、>、>=
- 等于、不等于:==、!=
- 逻辑与:&&
- 逻辑或:||
- 赋值运算:=、+=、-=、*=、/=、%=
四、Java流程控制
1、条件语句
简单if语句
语法格式:
if(条件){
条件成立时执行的代码
}
示例:
int age= 99;
if(age < 18){
System.out.println("未成年!");
}
简单if-else语句
if-else语句的操作比if语句多了一步;当条件成立时执行if部分的代码块,条件不成立时则进入else部分。
语法格式:
if(条件){
条件成立时执行的代码块
}else{
条件不成立时执行的代码块
}
示例:
int age= 99;
if(age < 18){
System.out.println("未成年人!");
}else{
System.out.println("成年人!");
}
多重if-else语句
多重if-else语句,当条件1不成立的情况下才会进入条件2进行判断,当前面的条件均不成立时才会执行else块内的代码。
语法格式:
if(条件1){
条件1成立时执行的代码块
}else if(条件2){
条件2成立时执行的代码块
}else{
条件1,2均不成立时执行的代码块
}
示例:
int age= 99;
if(age < 0){
System.out.println("输入的年龄有误");
}else if(age < 18)
System.out.println("未成年人!");
else{
System.out.println("成年人!");
}
switch-case语句
有时候可能需要对变量进行等值判断,则可以使用switch-case语句
语法格式:
switch(表达式){
case 值1:
执行代码块1
break;
case 值2:
执行代码块2
break;
case 值2:
执行代码块3
break;
default:
默认执行代码块
}
示例:
int age= 99;
switch(age){
case 18:
System.out.println("今年18岁");
break;
case 20:
System.out.println("今年20岁");
break;
case 22:
System.out.println("今年22岁");
break;
default:
System.out.println("??????");
}
2、循环语句
Java中常用的3种循环语句:while、do-while、for,循环语句可以重用代码使代码更精简,并且可读性好,更利于维护。
while语句
语法格式:
while(判断条件){
循环操作
}
执行流程:
- 1.先判断while后面的循环条件是否成立(真/假)。
- 2.当循环条件成立时,执行循环内的代码,然后重复执行1、2直到循环条件不成立为止。
do-while语句
语法格式:
do{
循环操作
}while(判断条件);
执行流程:
- 1.先执行一遍循环操作,然后判断循环条件是否成立(真/假)
- 2.如果循环条件成立,继续执行1、2,直到循环条件不成立为止。
for语句
语法格式:
for(循环变量初始化; 循环条件; 更新循环变量){
循环操作
}
执行流程:
- 1.初始化循环变量
- 2.判断循环条件是否成立(真/假)
- 3.当循环条件成立时,执行循环内的操作
- 4.更新循环变量
- 5.重复2、3、4,直到循环条件不成立为止。
注意:for循环后面括号中的三个表达式必须用";“分号隔开,三个表达式都可以省略但”;"分号不能省略。
增强for循环 foreach语句
foreach一般适用于对集合或数组的遍历,并且在循环遍历过程中不能对遍历对象进行修改(添加删除操作),如果想要修改则使用for循环。
语法格式:
for(元素类型 元素名称:遍历数组(集合)){
循环操作
}
示例:
List<String> arr = new ArrayList<String>();
arr.add("元素1");
arr.add("元素2");
arr.add("元素3");
for(String str: arr){
System.out.println(str);
}
break与continue语句
如果想主动退出循环可以使用break或continue语句
- break语句用于跳出整个循环语句
- continue语句主要用于跳过本次循环,执行下次循环。
五、数组(实际工作中常用List)
1、一维数组
数组是一组相同数据类型变量的集合,数组中的元素在内存中以连续存储的方法进行存储,数组的下标都是从0开始的。
数组的创建和初始化
数组的作用
- 存储相同数据类型的一组数据
- 对同类型数据集中存储、管理、便于遍历
数组的创建
方式一:声明
格式:
数组类型 数组名称[];
- 示例:int scores[];
数组类型[] 数组名称;
- 示例:int[] scores;
注意:声明数组时可以不设置数组长度。
方式二:分配空间
格式:
数据类型 数组名称[] = new 数组类型[数组长度];
- 示例:
- int scores[] = new int[10];
- String[] names = new String[10];
注意:数组元素根据类型不同,有不同的初始值
- int 类型,默认初始值为0
- String类型,默认初始值为null
- double类型,默认初始值为0.0
数组的赋值
方式一:先声明,再赋值(手动录入或动态从键盘录入)
int scores[] = new int[10];
scores[0] = 1;
scores[1] = 2;
方式二:声明并赋值
int scores[] = {88,99,77,66,55};
int scores[] = new int[10]{88,77,66...};
数组的遍历
方式一:使用for循环
int scores[] = {11,22,33,44,55};
for(int i=0;i<scores.length;i++){
System.out.println(scores[i]);
}
方式二:使用foreach循环
int scores[] = {11,22,33,44,55};
for(int score: scores){
System.out.println(score);
}
数组的简单排序
Arrays类方法排序
Arrays类是Java中提供的一个工具类,在java.util包中,该类包含了一些方法用来直接操作数组,可实现数组的排序、搜索等。
语法:Arrays.sort(数组名); //按升序进行排序
已知一个数组int[] num={32,66,55,45,87,83,74,95,99},现在从小到大进行排序
int[] nums={32,66,55,45,87,83,74,95,99};
Arrays.sort(nums); //升序排序
for(int i=0;i<nums.length;i++){
System.out.println(nums[i]);
}
数组元素的查找
已知一个数组int[] nums={32,66,55,45,87,83,74,95,99},请找出数组中有无95这个数,有则输出数组下标
方法一:线性查找法
在给定的值中进行搜索,从一端开始逐个检查每个元素,直到找到所需元素的过程。
int[] nums={32,66,55,45,87,83,74,95,99};
int index=-1;
for(int i=0;i<nums.length;i++){
if(nums[i] == 95){
index = i;
System.out.println("值:"+nums[i]+" 下标:"+index);
}
}
方法二:二分查找法
将数组先排序,在通过将数组中间位置的元素与查找关键字对比,如果两者相等则查找完成,否则利用中间位置将数组分为前、后两个子数组,如果中间位置的值大于查找关键字则进一步查找前子数组,否则进一步查找后子数组。重复以上过程,直到找到或找不到为止。
// 先排序,升序
Arrays.sort(nums);
for (int i = 0;i<nums.length;i++){
System.out.println("值:"+nums[i]+" 下标:"+i);
}
// 再查找
int index = -1;
int start = 0;
int end = nums.length-1;
int middle = 0;
do {
middle = (start+end)/2;
if(nums[middle] == 95){
index = middle;
System.out.println("找到 值:"+nums[index]+" 下标:"+index);
break;
}
if(nums[middle] < 95){
start = middle + 1;
}else {
end = middle - 1;
}
}while (start <= end);
2、二维数组
二维数组可以看成是以数组为元素的数组,Java中二维数组的声明和初始化应按照从高维到低维的顺序进行。
例如:
int[][] arr1 = new int[10][]; //一维长度未定
int[][] arr2 = new int[10][20];//一维、二维长度都确定
二数组创建并分配空间
格式:
方式一:
数组类型[][] 数组名 = new 数据类型[二维长度][一维长度]
方式二:
数组类型[][] 数组名;
数组名 = new 数据类型[二维长度][一维长度]
二维数组的赋值
二维数组的赋值和一维数组类似,可以通过下标来逐个赋值,注意数组索引从0开始。
格式:
数组名[二维长度][一维长度] = 值;
int[][] arr1 = new int[][]{{1,2,3},{3,2,1}};
arr1[0][0] = 6;
二维数组的遍历
二维数组的访问和输出同一维数组一样,只是多了一个下标而已。在遍历时需要再内嵌一个循环。
for (int i=0;i<arr1.length;i++){
for (int j=0;j<arr1[i].length;j++){
System.out.println("值:"+arr1[i][j]);
}
}
六、方法
1、什么是方法
Java方法是语句的集合,它们组合在一起完成一个功能。
2、方法的优点
方法的优点
- 方法可以使得程序变得更简短而清晰。
- 方法可以有利于程序的阅读维护。
- 方法提高了代码的重用性。
- 方法提高程序开发的效率。
3、方法的命名规则
一般方法的名字第一个字母应以小写字母作为开头,后面的单词则使用大写字母开头。例如:getName
4、方法的定义
方法的定义格式:
修饰符 返回值类型 方法名(参数类型 参数名[,参数类型 参数名...]){
...// 方法体
...// 返回值
}
5、方法的调用
当程序调用一个方法时,程序的控制权则交给了被调用的方法。当被调方法的返回语句执行完成后控制权交还给调用程序。
Java根据方法的返回值支持两种调用方式。
方式一:当方法返回一个值的时候,方法通常被当做一个值。
示例:
int a = max(30,40);
方式二:当方法没有返回值或返回值为void时,则方法的调用可以当做一条语句。
示例:
System.out.println("你好,世界");
一、Java类
Java类概述
定义一个类的一般步骤
- 指定类的访问属性
- 定义类的类名
- 编写类的属性
- 编写类的方法
示例:
package com.company; // 包名
//公开属性 类关键字 类名
public class CallPhone {
// 成员变量
float cpu;
float screen;
float mem;
// 成员方法
void call(){
System.out.println("打电话");
}
void sendMsg(){
System.out.println("发短信");
}
}
给类命名时,记住遵循编程风格:
- 1.如果给类名使用拉丁字母,那么名字的首字母使用大写字母
- 2.类名最好容易识别,见名知意。当类名由几个"单词"复合而成时,每个单词的首字母使用大写。
类体:类声明之后的一对大括号"{ }"以及他们之间的内容简称类体,大括号之间的内容称为类体的内容。
类体的内容由两部分构成:一部分是变量的声明,用来刻画类属性;另一部分是方法的定义,用来刻画类方法。
成员变量:
- 1.成员变量的类型:成员变量的类型可以是Java中的任何一种数据类型,包括基本类型:整型,浮点型,字符型;引用类型:数组,对象和接口。
- 2.成员变量的有效范围:成员变量在整个类内都是有效的,其有效性与它在类体中书写的先后位置无关。
- 3.编程风格:一行只声明一个变量,变量的名字符合标识符规定,变量名字见名知意,避免容易混淆的变量名。
- 4.成员变量可以在成员方法中直接使用,如果与方法的参数同名,则优先使用方法参数。
成员方法:方法的定义包括两部分:方法声明和方法体。
一般格式:
方法声明部分{
方法体的内容
}
类的实例化
类是面向对象语言中最为重要的一种数据类型,可以用它来声明变量。在面向对象语言中,用类声明的变量被称为对象。当使用一个类创建一个对象时也称实例化了这个类。通俗点讲就是类是对象的"模板",没有类就没有对象,构造方法和对象的创建密切相关。
对象的创建
-
类名 对象名 = new 类名()
CallPhone phone = new CallPhone();
对象的使用
-
使用对象的属性:对象名.属性
System.out.println(phone.cpu);
-
使用对象的方法:对象名.方法名
phone.call();
构造方法
构造方法是一种特殊方法,它的名字必须与它所在类的名字完全相同并且没有返回值类型。Java允许一个类中存在多个构造方法,但必须保证它们的参数不同,即参数的个数不同或者参数的类型不同。
需要注意的是
- 如果类中没有编写构造方法,编译器会默认该类只有一个构造方法,该默认的构造方法是无参数的,且方法体中没有语句。
- 如果类中定义了一个或多个构造方法,那么编译器将不提供默认的构造方法。
实例变量和类变量
实例变量和类变量的定义:
一个类的成员变量又分为实例变量和类变量。在声明成员变量时,用关键字static修饰的为类变量(类变量也称为静态变量),否则为实例变量。
实例变量和类变量的区别:
- 1.同一个类实例化出的不同对象的实例变量互不相关,都属于单独的变量。
- 2.同一个类实例化出的不同对象的类变量(静态变量)是共享的,就是不管实例化多少个对象,都只会有一份类变量。
- 3.通过"类名.类变量名"即可直接访问类变量(静态变量)。
- 4.实例变量不会自动化初始化,需要开发者手动初始化值。
- 5.静态变量默认会被初始化为0。
实例方法和类方法
实例方法和类方法的定义:
类中的方法也可以分为实例方法和类方法。方法声明时,方法类型前面不加关键字static修饰的是实例方法,加上static关键字修饰的是类方法(静态方法)。
实例方法和类方法的区别:
- 1.对象调用实例方法:当对象调用实例方法时,该方法中出现的实例变量就是分配给该对象的实例变量;该方法中出现的类变量也是分配给该对象的变量,只不过这个变量和所有的其他对象共享。
- 2.类名调用类方法:类方法不仅可以被类创建的任何对象调用执行,也可以直接通过类名调用。和实例方法不同的是类方法不可以操作实例变量,这是因为在类创建对象之前,实例成员变量还没有分配内存。
参数传递
参数传值:
方法中最重要的部分之一就是方法的参数,参数属于局部变量,当对象调用方法时,参数被分配内存空间,并要求调用者向参数传递值,即方法被调用时,参数变量必须有具体的值。
Java中方法的所有参数变量的值都是调用者指定的值的拷贝,例如,如果向方法的int型参数x传递一个int值,那么参数x得到的值是传递的值的拷贝。
基本数据类型的传值:
对于基本数据类型的参数,向该参数传递的值的数据类型不可以大于该参数的数据类型,参数传递属于自动类型转换。
引用类型参数的传值:
当参数是引用类型时,“传值"传递的是变量中存放的"引用”,而不是变量所引用的实体。
可变参数:
指在声明方法时不给出参数列表中从某项到最后一项参数的名称和个数,但这些参数的类型必须相同。可变参数使用"…"表示若干个参数,这些参数类型必须相同,最后一个参数必须是参数列表中的最后一个参数。
举例:public void f(int ... x);
二、封装
封装就是对类中的一些数据进行隐藏,来防止该类的方法和数据被外部类定义的代码随意访问。只有通过类开放的接口才能访问被保护的方法和数据。
封装的优点
封装的优点:
- 1.良好的封装能够减少耦合。
- 2.类内部的结构可以自由修改。
- 3.可以对成员变量的访问进行更精准的控制。
封装的实现
封装的实现:
- 1.修改成员变量的可见性来限制对成员变量的访问(一般设置为private)
- 2.创建getter/setter方法来对被保护的成员变量进行读取和写入
示例:
public class Person{
private String name;
private int age;
public int getAge(){
return age;
}
public String getName(){
return name;
}
public void setAge(int age){
this.age = age;
}
public void setName(String name){
this.name = name;
}
}
在上面这个类中将name和age成员变量的访问属性设置为了私有的,这样就只有本类才能访问,其他类都无法访问,以此来保护该成员变量数据的安全。同时该类提供了get*/set*类方法用来统一实现对私有数据的访问。
Java中的包
包是Java语言中有效管理类的一个机制。
包名的目的是有效的区分名字相同的类。不同Java源文件中两个类名字相同时,他们可以通过隶属于不同的包来相互区分。
包语句:通过关键字package声明包语句。
package语句作为Java源文件的第一条语句,为该源文件中声明的类指定包名。
package语句的一般格式为:
package 包名;
例如:
package sunrise;
package sun.com.cn;
注:
- 包的定义必须放在Java源文件的第一行
- 包名间可以使用"."号隔开
- 包名首字母不能为数字
包的使用:import语句
一个类可能需要另一个类来声明对象作为自己的成员或方法中的局部变量,如果这两个类在同一个包中,当然没有问题。
但如果一个类想要使用的那个类不在一个包中,要使用import语句完成导入。
使用import语句可以引入包中的类。在编写源文件的时候,处理自己编写的类以外,经常想要使用java提供的许多类,这些类可能在不同的包中,在学习Java语言时,使用已经存在的类,避免一切从头做起,这也是面向对象编程的一个重要方面。
为了能够使用Java提供给我们的类,可以使用import语句引入包中类。在一个Java源程序中可以有多个import语句,我们必须写在package语句(假如有package语句的话)和源文件中类的定义之间。Java为我们提供了大约130多个包,比如:
java.lang 包含所有的基本语言类
javax.swing 包含抽象窗口工具集中的图形,文本,窗口GUI类
java.io 包含所有的输入输出类
java.util 包含实用类
java.sql 包含操作数据库的类
java.nex 包含所有实现网络功能的类
java.applet 包含所有实现Java applet的类
示例:
import java.util.Arrays;
访问修饰符
访问权限:
当用一个类实例化一个对象之后,该对象可以通过".“运算符操作自己的变量,使用类中的方法,但对象操作自己的对象和使用类中的方法是有一定限制的。
所谓的访问权限就是指对象是否可以通过”.“运算符操作自己的变量或通过”."运算符使用类中的方法。
访问修饰包括private,protected和public,都是Java的关键字,用来修饰成员变量或方法。
访问修饰符
本类
同包
非同包子类
非同包非子类
private
有权访问
无权访问
无权访问
无权访问
默认(不加修饰)
有权访问
有权访问
无权访问
无权访问
protected
有权访问
有权访问
有权访问
无权访问
public
有权访问
有权访问
有权访问
有权访问
this关键字
this关键字表示当前对象,只可以出现在实例方法和构造方法中,不能出现在类方法中因为类方法可以通过类名直接调用,这时可能还没有任何对象诞生。
- this.成员变量:操作当前对象的成员变量
- this.成员方法:操作当前对象的成员方法
内部类
内部类(Inner Class)就是定义在另一个类中的类。与之对应的,包含内部类的类被称为外部类。
内部类的作用:
- 1.内部类提供了更好的封装,可以将内部类隐藏在外部类中,不允许同包中的其它类直接访问该类。
- 2.内部类的方法可以直接访问外部类的所有数据,包括私有的数据。
- 3.内部类所实现的功能使用外部类同样可以实现,只是有时候使用内部类实现会更方便。
内部类分类
成员内部类:
最常见的就是成员内部类,也叫做普通内部类。
必须使用外部类对象才能创建成员内部类对象,不能直接new一个内部类对象。
格式:
- 成员内部类 对象名 = 外部类对象.new 成员内部类();
如果外部类和内部类具有相同成员变量或方法,内部类可以直接访问内部类的成员变量或方法,但如果内部类需要访问外部类的成员变量或方法时,则需要使用this关键字
成员内部类示例:
// 外部类
public class Outer {
// 成员内部类
public class Inner{
// 成员内部类的方法
public void show(){
System.out.println("Inner!!");
}
}
public static void main(String[] args){
// 创建外部类对象
Outer outer = new Outer();
// 创建成员内部类对象
Inner inner = outer.new Inner();
// 调用成员内部类对象的方法
inner.show();
}
}
静态内部类:
静态内部类就是使用static关键字修饰的内部类,这种类的特点:
- 静态内部内不能直接访问外部类的非静态成员,但可以通过new 外部类().成员的方式访问。
- 创建静态内部类对象时,不需要外部类的对象,可直接创建。
格式: - 静态内部类 对象名 = new 静态内部类();
静态内部类示例:
// 外部类
public class Outer {
// 静态内部类
public static class StaticInner{
// 静态内部类的方法
public void show(){
System.out.println("StaticInner!!");
}
}
public static void main(String[] args){
// 创建静态内部类对象
StaticInner staticInner = new StaticInner();
// 调用静态内部类方法
staticInner.show();
}
}
方法内部类:
方法内部类就是将类定义在外部类的方法中,方法内部类只能在该方法内有用。
方法内部类示例:
// 外部类
public class Outer {
// 方法内部类
public void function(){
class FuncInner{
// 方法内部类的方法
public void show(){
System.out.println("FuncInner!!");
}
}
// 方法内部类的创建
FuncInner funcInner = new FuncInner();
funcInner.show();
}
}
匿名内部类:
匿名内部类可以在定义一个类的同时对这个类进行实例化,如果某个类只需要使用一次,那么就可以使用匿名内部类来简化代码,使代码更简洁。
匿名内部类示例:
// 外部类
public class Outer {
// 成员内部类
public class Inner{
// 成员内部类的方法
public void show(){
System.out.println("Inner!!");
}
}
public static void main(String[] args){
// 创建外部类对象
Outer outer = new Outer();
// 定义匿名内部类并实例化
outer.new Inner(){
public void show(){
System.out.println("Anonymous Inner!!");
}
}.show();
}
}
三、继承
继承是类与类之间的一种关系,Java中的继承是单继承的。
继承的好处:
实现代码复用:子类拥有父类的所有属性和方法,并可以根据实际情况添加自己的新属性与方法。
由继承得到的类称为子类,被继承的类称为父类或超类。
声明一个子类的语法格式:
class 子类名 extends 父类名{
... ...
}
匿名类
与子类相关的匿名类
假如没有显式的声明一个类的子类,而又想用子类创建一个对象,那么如何实现这一目的?
Java允许直接使用这一个类的子类的类体创建一个子类对象,也就是说创建子类对象时,除了有父类的构造方法外还有类体,此类体被认为是一个子类去掉类声明后的类体,称为匿名类。
假如有一个Animal类,声明如下:
class Animal{
public void move(){
System.out.println("动物可以移动");
}
}
那么下列代码就是用Animal的一个子类(匿名类)创建对象:
new Animal(){
@Override
public void move() {
System.out.println("go go go go");
}
};
匿名类的常见使用方式是在给方法进行参数传值时。
比如有一个方法声明如下:
void f(Animal animal){
animal.move();
}
那么可以直接创建一个匿名类对象并传递给该方法
f(new Animal(){
@Override
public void move() {
System.out.println("go go go go");
}
});
方法重写
方法重写是指子类需要定义一个方法,这个方法的返回值类型、方法的名字、方法参数个数,参数的类型都和父类的方法完全相同,这时就需要用到方法重写,当调用时会优先调用子类重写后的方法。
重写的目的
子类通过方法的重写可以隐藏继承的方法,子类通过方法的重写可以把父类的状态和行为改为自身的状态和行为。
使用示例
class Animal{
public void move(){
System.out.println("动物可以移动");
}
}
class Dog extends Animal{
// 重写父类的move方法
public void move(){
System.out.println("狗可以跑和走");
}
}
注意:重写父类的方法时,不可以降低方法的访问权限。
继承初始化顺序
顺序从先到后
- 1.父类对象
- 2.父类属性初始化
- 3.父类构造方法
- 4.子类对象
- 5.子类属性初始化
- 6.子类构造方法
final的使用
final关键字作为标识有"最终的"含义,final可以用来修饰类、方法、属性和变量。
- final修饰类,则该类不允许被继承。
- final修饰方法,则该方法不允许被重写。
super的使用
super关键字:
在对象的内部使用,可以代表父类对象。
1.访问父类的属性
- super.属性名
2.访问父类的方法
- super.方法名
super关键字的应用:
1.子类构造的过程中必须调用父类的构造方法。
2.如果显式调用构造方法,必须在子类的构造方法中的第一行。
3.如果子类构造方法中既没有显式调用父类构造方法,而父类又没有无参的构造方法,则编译出错。
Object类
Object类是所有类的超类,如果一个类没有使用extends关键字明确标识继承另一个类,那么这个类就默认继承Object类。
Object类中的方法适合所有子类。
- toString()返回对象地址字符串:一般会重写该方法,返回对象的属性。
- equals()比较对象引用的地址:一般会重写该方法,比较对象的属性值。
四、多态
引用的类型上转换
向上类型转换(隐式/自动类型转换),小类型到大类型转换时会触发。
假设,A类是B类的父类,当子类B创建一个对象,并把这个对象的引用赋值给父类A时,此时这个父类A的对象就称之为子类B创建的对象的上转型对象。
举例:
A a;
B b = new B();
a = b;
同理可得:
A a;
a = new B();
或
A a = new B();
注意:
- 1.不要将父类创建的对象和子类对象的上转型对象混淆。
- 2.可以将对象的上转型对象再强制转换到一个子类对象,这时,该子类对象又具备了子类所有属性和功能。
- 3.不可以将父类创建的对象的引用赋值给子类声明的对象。
多态性
多态性就是指父类的某个实例方法被子类重写后,可以各自产生自己独有的功能。比如当一个类有多个子类,并且这些子类都重写了父类的某个实例方法,那么将这些子类创建的对象上转型到父类对象得到该子类对象的上转型对象,这个上转型对象在调用这个实例方法时就具有多种心态,也就是多态性。
抽象类和抽象方法
用关键字abstract修饰的类称为abstract类(抽象类)
如:
public abstract class A{
...
}
用关键字abstract修饰的方法我们称为abstract方法(抽象方法)
如:
abstract int main(int x,int y);
抽象类与普通类的区别:
- 1.和普通类相比,抽象类可以有abstract方法,也可以有非abstract方法。
- 2.抽象类,只允许声明,不能使用new关键字进行实例化。
- 3.如果一个非抽象类是某个抽象类的子类,那么必须重写父类的abstract方法,给出方法体。
- 4.如果一个抽象类是抽象类的子类,它可以重写父类的abstract方法,也可以继承这个abstract方法,但依然不能实例化。
面向抽象编程:
在设计一个程序时,可以通过在abstract类中声明若干个abstract方法,表明这些方法在整个系统设计中的重要性,方法体的内容细节由它的非abstract子类去完成。
使用多态进行程序设计的核心技术之一就是使用上转型对象,即将abstract类声明对象作为子类的上转型对象,那么这个上转型对象就可以调用子类重写的方法。
所谓面向抽象编程,是指当设计某种重要的类时,不让该类面向具体的类,而是面向抽象类,即所设计类中的重要数据是抽象类声明的对象,而不是具体类声明的对象。
抽象是指从具体事物抽出、概括出它们共同的方面、本质属性与关系等,而将个别的、非本质的方面、属性与关系舍弃的思维过程。
接口
Java除了平台无关的特点外,接口也是该语言的又一特色。Java舍弃了C++语言中多重继承的机制,使得编写的代码更加健壮和便于维护。
Java不支持多继承,即一个类只能有一个父类。单继承性使得Java简单,易于管理和维护。那么为了克服Java单继承的缺点,Java使用了接口,一个类可以实现多个接口,Java的接口更符合人的思维方式。
接口声明:
使用关键字interface来定义一个接口。
接口的定义和类的定义很类似,分为接口的声明和接口体。
public interface 接口名 [extends 父接口1, extends 父接口2,...]{
// 常量
// 抽象方法
...
}
接口体:
接口体包含常量的声明(没有变量)和抽象方法声明(没有普通方法)两个部分。接口体中只有抽象方法,没有普通方法,而且接口体中所有的常量的访问权限一定是public(允许省略public,final修饰符),所有抽象方法的访问权限一定都是public(允许省略public,abstract修饰符)
举例:
interface Test{
public final int MAX = 100;//等价写法:int MAX =100;
public abstract void add( );//等价写法:void add();
public abstract float getSum(float x,float y);
}
类通过使用关键字implements声明自己实现一个或多个接口,如果实现多个接口,则用逗号隔开接口名
class A implements Printable,Addable
class Dog extends Animal implements Eatable,Sleepable
如果一个类实现了某个接口,那么这个类必须重写该接口的所有方法,因为这些方法都是abstract方法。
理解接口:
接口可以增加很多类都使用到的具体的功能,不同的类可以实现相同的接口,同一个类也可以实现多个接口。
接口只关心操作,并不关心操作的具体实现。
接口的思想在于它可以增加很多类都需要具体有的功能,而且实现相同的接口类不一定有继承关系。
接口回调:
接口回调是指可以把实现某一接口的类创建的对象赋值给该接口声明的接口变量中,那么该接口变量就可以调用被类重写的接口方法。(上转型)
接口的多态性:
把实现接口的类的实例的引用赋值给接口变量后,该接口变量就可以回调类重写的接口方法。
由接口产生的多态就是指不同的类在实现同一个接口时可能具有不同的实现方法,那么接口变量在回调接口方法时就可能具有多种形态。
接口和abstract类的比较如下:
- 1.abstract类和接口都可以有abstract方法。
- 2.接口中只可以有常量,不能有变量,而abstract类中即可以有常量也可以有变量。
- 3.abstract类中也可以有非abstract方法,接口不可以。
面向接口编程:
面向接口去设计程序,可以在接口中声明若干个abstract方法,表明这些方法的重要性,方法体的内容细节由实现接口的类去完成。
使用接口进行程序设计的核心思想就是使用接口回调,即接口变量存放实现该接口的类的对象的引用,从而接口变量就可以回调类实现的接口方法。
五、异常
什么是异常
所谓的异常就是程序运行时可能出现一些错误,比如试图打开一个根本不存在的文件等,异常处理将会改变程序的控制流程,让程序有机会对错误做出处理。
异常分类
Throwable类是Java语言中所有错误或异常的超类。
Error、RuntimeException及其子类、其他异常表示一种运行时的困难,它通常由环境引起,可以进行处理。
常见异常类
ArithmeticException:出现异常的运算条件时,抛出该异常,例如一个整数"除以零"时。
ArrayStoreException:试图将财务类型的对象存储到一个对象数组时抛出该异常。
ClassCastException:当试图将对象强制转换为不是实例的子类时,抛出该异常。
IndexOutOfBoundsException:当对数据进行索引时(例如数组、字符串),超出了可访问范围将会抛出该异常。
NullPointerException:当应用程序试图在需要对象的时候使用NULL时,将会抛出该异常。
捕获异常和抛出
try-catch语句
使用try-catch捕获异常并进行处理,将可能出现的异常操作放在try-catch语句的try部分,将发生异常后的处理代码放在catch部分。
语法格式:
try{
// try代码块中的代码有可能抛出异常
}catch(ExceptionSubClass1 e){
// 处理try代码块中抛出的ExceptionSubClass1类型的异常实例e
}catch(ExceptionSubClass2 e){
// 处理try代码块中抛出的ExceptionSubClass2类型的异常实例e
}
finally语句
finally语句在try-catch-finally中是一个可选的部分。该语句定义一个总是被执行的代码块,而不考虑异常是否被捕获,所以它提供了在try-catch代码块执行结束后的处理机制。
示例:
try{
// try代码块中的代码有可能抛出异常
}catch(ExceptionSubClass1 e){
// 处理try代码块中抛出的ExceptionSubClass1类型的异常实例e
}catch(ExceptionSubClass2 e){
// 处理try代码块中抛出的ExceptionSubClass2类型的异常实例e
}finally{
// 无论是否有异常抛出,此部分的代码总是被最后执行
}
注意:一个try块可以不执行finally子句就退出的唯一方法是通过调用System.exit()方法来实现。
异常抛出
throw关键字可以用来抛出一个异常。
捕获所有异常
如果我们不确定可能抛出的异常,那么可以使用Exception类来捕获所有异常。
try{
// try代码块中的代码有可能抛出异常
}catch(Exception e){
// 处理try代码块中抛出的所有异常
}
自定义异常
我们也可以扩展Exception类或其子类来定义属于自己的异常类,这种异常类可以包含一般类所能包含的任何东西。
方法声明中的异常列表
一个方法在声明时可以使用throws关键字声明该方法可能抛出的若干个异常。并在方法体中出现异常的位置给出产生异常的操作,即用相应的异常类创建对象,并使用throw关键字抛出该异常对象,导致该方法结束执行。
格式:
返回类型 方法名([type para1[,...]]) throws Exception1[,Exception2...]{
...// 方法体
}
注意:一个方法可以抛出多个异常,异常之间用逗号隔开,但是一个方法在某一时刻只能抛出一个异常,不能同时抛出多个异常。
使用异常的注意点
- 对捕获到的异常需要有针对性的处理,如错误恢复机制,不要捕获到了异常却没有实质性的处理,异常并不是调试。
- 分析可能出现异常的代码,不将无关的代码放入try代码块中,为了求得心理安慰而使得try代码块变得过于庞大,这样做只会导致出现异常而无法确定异常的位置。
- 尽量不要使用Exception试图捕获所有的异常,要注意不同的异常要有针对性处理。
- 尽量利用异常而不是返回值来揭示程序执行中的状态,也不能用异常来代替简单的测试和逻辑判断。
Java常用类
String类
String类在java.lang包中,是Java专门提供用来处理字符串的类,由于java.lang包中的类是默认被引入的,因此程序可以直接使用String类。
String类用来创建一个字符串变量,字符串变量是对象。
方式一:
String str="你好";
方式二:
String str2 = new String("你好");
使用"+"号将两个字符串连接在一起
String str = new String("世界"+"你好");
需要注意字符串变量的改变只是改变了指向的内存地址,并不会对实际字符串进行修改,字符串在内存中是以常量的形式存在,所以如果频繁对String类进行修改就会导致内存中存在大量的垃圾数据。所以常使用StringBuffer类来存储需要频繁改变的字符串。
常用方法:
- public int length( ):获取一个字符串的长度
- public boolean equals(String s):比较当前字符串对象的实体是否与参数s指定的字符串实体相同
- public boolean startsWith(String s):判断当前字符串对象的前缀(后缀)是否为参数s指定的字符串
- public boolean contains(String s):判断当前字符串对象是否含有参数指定的字符串s
- public int indexOf(String s):从当前字符串的头开始检索字符串s,并返回首次出现s的位置
- public String substring(int startpoint):获得一个当前字符串的字串
- public String trim():得到一个去掉前后空格后的字符串对象
- public char[] toCharArray():将字符串中的全部字符存放到一个字符数组中
- public byte[] getBytes():使用平台默认的字符编码,将当前字符串转化为一个字符数组
StringBuffer类
StringBuffer类,该类能够创建可修改的字符串对象,也就是说,该类对象的实体的内存空间可以自动的改变大小,便于存放一个可变的字符序列。
举例:
StringBuffer buffer = new StringBuffer("我喜欢");
buffer.append("玩篮球"); //将字符串连接
常用方法:
- public StringBuffer append(String s):将指定的字符串追加到此字符序列
- public StringBuffer reverse():将此字符序列用其反转形式取代
- public delete(int start,int end):移除此序列的子字符串中的字符
- public insert(int offset,int i):将int参数的字符串表示形式插入此序列中
- public insert(int offset,String str):将str参数的字符串插入此序列中
- public replace(int start,int end,String str):使用给定的str字符串替换此序列中指定范围内的字符
- public int capacity():返回当前容量
- public int length():返回长度(字符数)
File类
java.io.File类用于文件或目录的描述信息,例如生成新目录、修改文件名、删除文件、判断文件是否存在等。
该类对文件系统中文件以及文件夹进行了封装,可以通过对象的思想来操作文件和文件夹。包括文件名、文件长度、最后修改时间等一系列属性。
常用构造方法:
- File(String pathname):创建一个指定的文件。
- File(String parent,String child):在指定的目录下创建文件。
- File(File parent,String child):在指定的目录下创建文件。
创建File对象成功后可以使用以下常用方法: - public boolean exists():判断文件或目录是否存在
- public boolean isFile():判断是文件还是目录
- public boolean isDirectory():判断是文件还是目录
- public String getName():获取文件名或目录名
- public String getPath():获取文件路径或目录路径
- public String getParent():获取文件父路径
- public long length():获取文件的长度
- public String[] list():获取一个包含目录中所有文件名的字符串数组
- public boolean renameTo(File newFile):重命名文件
- public void delete():删除文件
- public boolean mkdir():创建目录,如果父目录不存在则异常
- public boolean mkdirs():创建目录,如果不存在父目录则一起创建
- public boolean createNewFile:创建文件
IO操作
什么是IO
什么是IO,即输入(Input)/输出(Output),输入就是程序读取外部数据(文件、网络、键盘)到程序(内存)中,输出就是将程序(内存)中的数据输出给外部(文件、网络、键盘)。
Java中将数据的输入输出抽象为流,像水一样有顺序、单向的、有起点和终点的数据集合。
流的分类
流的分类方式很多,根据流的最小数据单位可以将流分为字节流,字符流。
- 字节流:以8位(1byte,8bit)作为一个数据单位,数据流中最小数据单位为一个字节。(也就是C/C++里的二进制读写)
- 字符流:以16位(2byte,16bit)作为数据单位,数据流中最小数据单位为一个字符。Java中的字符是Unicode编码,所以一个字符占两个字节。
根据流的流向将流分为输入流、输出流。
-
输入流:InputStream,Reader,属于抽象类,是所有输入流的父类,定义了所有输入流都具有的共同特征。
-
输出流:OutputStream,Writer,属于抽象类,是所有输出流的父类,定义了所有输出流都具有的共同特征。
-
字节输入流InputStream的三个基本读方法
- abstract int read():读取一个字节的数据并返回读到的数据,当读到输入流的末尾时方法返回-1
- int read(byte[] b):将读取读取到byte数组中,同时返回实际读取的字节数,如果返回-1,表示读到了输入流的末尾
- int read(byte[] b,int off,int len):将数据读入到byte数组,同时返回实际读取的字节数,如果返回-1,表示读取到了末尾,off指定在数组中存放数据的起始偏移位置,len指定读取的最大字节数
-
字节输出流OutputStream中的三个基本的写方法
- abstract void write(int b):往输出流中写入一个字节
- void write(byte[] b):往输出流中写入数组b中的所有字节
- void write(byte[] b,int off,int len):往输出流中写入数组b中从偏移off开始的len个字节的数据
-
字符输入流Read基本的三个读方法:
- public int read() throws IOException:读取一个字符,返回值为读取的字符
- public int read(char cbuf[]) throws IOException:读取一系列字符到数组cbuf[]中,返回值为实际读取的字符数量。
- public abstract int read(char cbuf[],int off,int len) throws IOException:读取len个字符,从数组cbuf[]的下标off处开始存储,返回值为实际读取的字符数。
-
字符输出流Writer主要写方法:
- public void write(int c) throws IOException:写单个字符到输出流
- public void write(char cbuf[]) throws IOException:将字符数组cbuf[]写到输出流
- public abstract void write(char cbuf[],int off,int len) throws IOException:将字符数组cbuf[]中的从索引未off的位置处开始的len个字符写入输出流
- public void write(String str) throws IOException:将字符串str中的字符写入输出流
- public void write(String str,int off,int len) throws IOException:将字符串str中从索引off开始处的len个字符写入输出流
还能根据流是否直接处理数据将流分为节点流、处理流。节点流是直接处理数据的,处理流是装饰加工节点流的。
节点流:
- 文件流:FileInputStream,FileOutputStrean,FileReader,FileWriter,它们都会直接操作文件。因此他们被称为- -节点流 ,注意:使用这几个流对象之后,需要关闭流对象,因为java垃圾回收器不会主动回收。不过在Java7 之后,可以在try() 括号中打开流,最后程序会自动关闭流对象,不需要显示地close。
- 数组流:ByteArrayInputStream,ByteArrayOutputStream,CharArrayReader,CharArrayWriter,对数组进行处理的节点流。
- 字符串流:StringReader,StringWriter,其中StringReader能从String中读取数据并保存到char数组。
- 管道流:PipedInputStream,PipedOutputStream,PipedReader,PipedWrite,对管道进行处理的节点流。
处理流:
- 缓冲流 :BufferedInputStrean,BufferedOutputStream,BufferedReader ,BufferedWriter,增加缓冲功能,避免频繁读写硬盘,可以初始化缓冲数据的大小,由于带了缓冲功能,所以就写数据的时候需要使用flush方法,另外BufferedReader 提供一个readLine( )方法可以读取一行,而FileInputStream和FileReader只能读取一个字节或者一个字符,因此BufferedReader也被称为行读取器。
- 转换流:InputStreamReader,OutputStreamWriter,要InputStream或OutputStream作为参数,实现从字节流到字符流的转换,经常在读取键盘输入System.in或网络通信的时候,需要使用这两个类。
- 数据流:DataInputStream,DataOutputStream,提供将基础数据类型写入到文件中,或者读取出来。
常见IO操作
一、使用字符文件流读写文件
/**
* Java IO:使用文件流读写文件
* **/
public static void ReadFileAndWriteFile(){
// 建立一个输入流对象,将已存在的一个文件加载到输入流里面
FileReader fr = null;
FileWriter fw = null;
try {
fr = new FileReader(new File("src/com/company/read.txt"));
fw = new FileWriter(new File("src/com/company/write.txt"));
// 创建一个临时存放数据的数组
char[] buf = new char[1024];
int len;
// 调用输入流对象的读取方法将输入流的数据读取到数组中
while ((len = fr.read(buf)) != -1){
System.out.print(new String(buf,0,len)); //将读取到的byte型数组转为String并打印
fw.write(buf,0,len); // 将读取到的内容写入到文件
fw.write("写入文件完成");
fw.flush(); // 手动将缓冲区强制写入文件
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fr != null){
try {
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fw != null){
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
二、使用缓存流读写数据
缓存流就是带缓存功能的流、在使用这类缓存流类时,会创建一个内部缓冲区数组,默认是8192个字节的缓冲区。需要注意的是使用缓冲流时需要将缓存流绑定到节点流上,根据数据操作单位可以将缓冲流分为:
-
BufferedInputStream和BufferedOutputStream
-
BufferedReader和BufferedWriter
/** * Java IO:使用缓存流读写文件 * **/ public static void BuffReadFileAndBuffWriteFile(){ BufferedReader br = null; BufferedWriter bw = null; try{ // 创建缓冲区流对象:缓冲区流属于处理流,是对节点流的包装 br = new BufferedReader(new FileReader("src/com/company/buff_read.txt")); bw = new BufferedWriter(new FileWriter("src/com/company/buff_write.txt")); String line; while ((line = br.readLine()) != null){ //一次读取字符文本文件一行字符 bw.write(line); // 一次写入一行字符串 bw.newLine(); // 写入行分隔符 } bw.flush(); // 刷新缓冲区 } catch (IOException e) { e.printStackTrace(); }finally { if(br != null){ try { br.close(); } catch (IOException e) { e.printStackTrace(); } } if(bw != null){ try { bw.close(); } catch (IOException e) { e.printStackTrace(); } } } }
三、使用转换流从键盘读取数据并在屏幕输出。
/**
* Java IO:从键盘输入并输出到屏幕。
* **/
public static void keyInputAndPrintConsole() throws IOException {
PrintWriter out = null;
BufferedReader br = null;
try{
System.out.println("请输入数据:");
out = new PrintWriter(System.out,true);
br = new BufferedReader(new InputStreamReader(System.in));
String line = null;
while ((line = br.readLine()) != null){
if(line.equals("exit")){
System.exit(1);
}
out.println(line);
}
}catch (IOException e) {
e.printStackTrace();
}finally {
out.close();
br.close();
}
}
Java IO常见面试题
一、字节流和字符流的区别?
读写单位不同:字节流最小读写单位是一字节,而字符流最小读写单位是一字符。
处理对象不同:字节流能处理所有类型的数据(也就是二进制的),而字符流只能处理字符类型的数据。
字节流没有缓冲区,是直接输出的,而字符流是先输出到缓冲区,因此在输出时,字节流没调用colse()方法前数据就已经输出了,而字符流只有在调用colse()方法关闭缓冲区时才会输出,不过可以通过手动调用flush()方法来强制输出。
二、什么是对象序列化,什么是反序列化,实现对象序列化需要做哪些工作?
对象序列化:将对象以二进制的形式保存到磁盘。
反序列化:将二进制的数据转化为对象。
实现serializable接口的类可以实现对象序列化,其中没有需要实现的方法,implements Serializable只是为了标注该对象是可被序列化的。
泛型
泛型,即"类型参数化"。说到参数我们的第一反应可能是定义方法时的形参,或者是调用方法时的实参。那么"类型参数化"的意思也就是将原来的具体类型变成类似方法中的参数一样,使用时才传入具体的类型。定义时的泛型参数可以叫做泛型类的形式类型,使用时的泛型参数可以叫做泛型类的实际类型,跟C/C++中的模板有点像。
泛型的目的
Java泛型的主要目的是可以建立具有类型安全的数据结构,如链表,散列表等数据结构,最重要的一个优点是:在使用这些泛型建立数据结构时,不必进行强制类型转换,即运行时不进行类型检查。
JDK1.5是支持泛型的编译器,它将运行时的类型检查提到编译时执行,使代码更安全。
泛型类
泛型类型用在类定义中,这个类被称为泛型类。
定义格式:
class 类名称 <泛型标识>{
private 泛型标识 /*(成员变量类型)*/ var;
.....
}
}
示例:
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{
//key这个成员变量的类型为T,T的类型由外部指定
private T key;
public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
this.key = key;
}
public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
return key;
}
}
//泛型的类型参数只能是类类型(包括自定义类),不能是简单类型
//传入的实际类型需与泛型类的形式类型的类型相同,即为Integer.
Generic<Integer> genericInteger = new Generic<Integer>(123456);
//传入的实际类型需与泛型类的形式类型的类型相同,即为String.
Generic<String> genericString = new Generic<String>("key_vlaue");
Log.d("泛型测试","key is " + genericInteger.getKey());
Log.d("泛型测试","key is " + genericString.getKey());
注意:
定义的泛型类,实例化时不一定必须传入泛型实参,只有在传入了泛型实参的情况下,编译才会根据传入的泛型实参起到相应的限制作用,如果不传入泛型实参,那么泛型类中使用泛型的方法或成员变量定义的类型可以是任何类型。
泛型接口
泛型接口与泛型类的定义和使用基本相同。
定义格式:
//定义一个泛型接口
public interface 类名称 <泛型标识> {
public 泛型标识 next();
}
示例:
//定义一个泛型接口
public interface Generator<T> {
public T next();
}
使用泛型接口:
方式一、实现泛型接口类,但不传入泛型实参:
/**
* 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
* 即:class FruitGenerator<T> implements Generator<T>{
* 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
*/
class FruitGenerator<T> implements Generator<T>{
@Override
public T next() {
return null;
}
}
方式二、实现泛型接口类,并且传入泛型实参:
/**
* 定义类实现泛型接口,虽然我们只创建了一个泛型接口Generator<T>,但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
* 类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
* 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。
*/
public class FruitGenerator implements Generator<String> {
private String[] fruits = new String[]{"Apple", "Banana", "Pear"};
@Override
public String next() {
Random rand = new Random();
return fruits[rand.nextInt(3)];
}
}
泛型方法
定义格式:
public <泛型标识> 返回值 方法名(方法参数)throws InstantiationException ,
IllegalAccessException{
T instance = tClass.newInstance();
return instance;
}
示例:
public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
IllegalAccessException{
T instance = tClass.newInstance();
return instance;
}
public与返回值中间表示声明此方法为泛型方法。
泛型类中的使用了泛型的成员方法只是普通的成员方法,并不是泛型方法,只有声明了的方法才是泛型方法。
与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
泛型通配符
泛型的类型通配符一般使用"?“代替具体的类型实参,注意此处”?“是类型实参,而不是类型形参 。当具体类型不确定,不需要使用类型的具体功能时,只使用Object类中的功能时,那么可以使用这个通配符”?"当泛型的实参类型,表示未知类型。
集合
在Java语言中,Java语言的设计者对常用的数据结构和算法做了一些规范(接口)和实现(具体实现接口的类)。所有抽象出来的数据结构和操作(算法)统称为Java集合框架。
Java程序员在具体应用时,不必考虑数据结构和算法实现细节,只需要用这些类创建出来一些对象,然后直接引用到就可以了,这样大大提高了编程效率。
List、Set、Map
List集合:有序集合,允许存放重复的元素。
List接口的实现类:
- ArrayList:底层实现了一个可变大小的数组,所以具有数组的查询速度快的优点,这也导致增删改查速度慢。允许所有元素,包括null,每个ArrayList实例都有一个容量(Capacity)即用于存储元素的数组的大小,容量随着不断增加新元素来自动增加。当需要插入大量元素时,在插入前可以调用ensureCapacity方法来增加ArrayList的容量以提高插入效率。
- LinkedList:底层实现了一个双向循环链表,每个链表节点都由三部分组成:前指针、数据、后指针,双向链表的查询效率低但增删改查效率高,允许null元素,此外LinkedList提供额外的get,remove,insert方法在LinkedList的首部或尾部。这些操作使LinkedList可被用作堆栈(stack),队列(queue)或双向队列(deque)
- Vector:与ArrayList类似,区别是Vector是重量级组件,消耗的资源比较多。一般在考虑并发的情况下会使用Vector。
Set:无序集合,不保存重复的元素,因此存入Set的每个元素都必须是唯一的。
- HashSet:不允许出现重复元素,不保证集合中元素的顺序,允许包含值为null的元素,但最多只能有一个null元素
- TreeSet:底层是采用红黑树实现、元素唯一且已经排好序
Map:Java为数据结构中的映射定义了一个接口Java.util.Map;它有4个实现类,Map主要用于存储键值对,根据键得到值,因此不允许键重复,但允许值重复。
- HashMap:键值对,key不能重复,但是value可以重复。key的实现就是HashSet,value对应着key存放,允许null的键或值。
- HashTable:不允许null的键或值。
- LinkedHashMap:与HashMap的不同之处在于LinkeHashMap维护着一个运行于所有条目的双重链表列表,存储的数据是有序的。
- TreeMap:将key排好序的Map,key使用TreeSet实现,value对应着key存放。
多线程机制
Java多线程
Java语言的一大特点就是内置对多线程的支持。
Java虚拟机控制从一个线程切换到另一个线程。这些线程将被轮流执行,使得每个线程都有机会使用CPU资源。
每个Java应用程序都有一个默认的主线程。JVM一直要等到Java应用程序中的所有线程都结束之后,才结束Java应用程序。
在Java中实现多线程有两种方式,一种是基础Thread类,另一种是实现Runnable接口。
Thread类实现多线程
使用Thread的子类创建线程:在编写Thread类的子类时,需要重写父类的run( )方法,其目的是规划线程的具体操作,否则线程就什么也不做,因为父类的run( )方法中没有任何操作语句。
示例:
package com.company;
/**
* 多线程实现一:继承Thread类
**/
class MyThread1 extends Thread {
private String thread_name;
MyThread1(String thread_name){
this.thread_name = thread_name;
}
public String getThread_name() {
return thread_name;
}
@Override
public void run() {
for (int i=0;i<10;i++){
System.out.println(thread_name+":"+i);
}
}
}
public class Main {
public static void main(String[] args) {
MyThread1 myThread1 = new MyThread1("t1");
MyThread1 myThread2 = new MyThread1("t2");
myThread1.start();
myThread2.start();
}
}
使用Thread子类创建线程的优缺点是:可以在子类中增加新的成员变量,使线程具体有某个属性,也可以在子类中新增方法,使线程具有某种功能。但是,Java不支持多继承,Thread类的子类不能再扩展其他的父类。
Runnable接口实现多线程
使用Runnable接口创建线程:创建线程的另一个途径就是用Thread类直接创建线程对象。使用Thread创建线程通常使用的构造方法是:
Thread(Runnable target),该构造方法中的参数是一个Runnable类型的接口。
在创建线程对象时必须向构造方法的参数传递一个实现Runnable接口类的实例,该实例对象称为所创建线程的目标对象,当线程调用start( )方法后,一旦轮到它来享受CPU资源,那么目标对象就能自动调用接口中的run( )方法。
示例:
package com.company;
/**
* 多线程实现二:实现Runnable接口
* **/
class MyThread2 implements Runnable{
private String thread_name;
MyThread2(String thread_name){
this.thread_name = thread_name;
}
public String getThread_name(){
return thread_name;
}
@Override
public void run() {
for (int i=0;i<10;i++){
System.out.println(thread_name+":"+i);
}
}
}
public class Main {
public static void main(String[] args) {
MyThread2 myThread21 = new MyThread2("t1");
Thread t1 = new Thread(myThread21);
Thread t2 = new Thread(myThread21);
t1.start();
t2.start();
}
}
两种创建方式(继承Thread和实现Runnable接口)有什么区别?
- 1.一个类只能继承一个父类,存在局限性;一个类可以实现多个接口。
- 2.在实现Runable接口调用Thread(Runnable target)创建线程时,使用同一个Runnable实例,则建立的多线程实例变量也是共享的。但通过继承Thread类创建线程是不能用一个实例建立多个线程,故而实现Runnable接口适合于资源共享。当然继承Thread类也能够共享变量,不过只能共享Thread类的static变量;
- 3.Runnable接口和Thread之间的联系:通过查看源码:public class Thread extends Object implements Runnable,可以看出Thread类实现的也是Runnable接口;
线程状态
Java中线程在它的一个完整的生命周期中通常要经历以下几种状态:
- 初始状态:当一个Thread类或其子类的对象被声明并实例化时,新生的线程对象处于初始状态,此时已经有了相应的内存空间和其他资源,但处于不可运行状态。
- 就绪状态:调用start()方法(从父类继承的方法)通知JVM后,JVM知道又有一个新线程排队等待切换了。一旦轮到他来享受CPU资源时,此线程就可以脱离创建它的主线程独立开始自己的生命周期了。
- 运行状态:当准备状态的线程获得CPU资源时,线程就进入运行状态,此时自动调用该线程对象的run()方法,run()方法定义了该线程的实际功能。
- 中断状态:有4种原因的中断:
- JVM将CPU资源从当前线程切换给其他线程,使得本线程让出CPU的使用权处于中断状态。
- 线程使用CPU资源期间,执行了sleep()方法,使得当前线程进入休眠状态。
- 线程使用CPU资源期间,执行了wait()方法,使得线程进入等待状态。
- 线程使用CPU资源期间,执行某个操作进入阻塞状态。
- 死亡状态:线程调用stop()方法或run()方法执行结束后,即处于死亡状态,该状态的线程不具有继续运行的能力。
问:Java程序每次运行至少会启动几个线程?
- 至少启动两个,一个是main线程,另一个是垃圾回收线程(GC线程)。
线程同步
在处理多线程问题时,我们必须注意这样一个问题:当两个或多个线程同时访问同一个变量,并且一个线程需要修改这个变量时,我们应该如何对该问题作出处理?
所谓线程同步就是若干个线程都需要使用一个synchronized修饰的方法,即程序中的若干个线程都需要使用一个方法,而这个方法用synchronized给予修饰。
多个线程调用synchronized方法必须准守同步机制:当一个线程使用该方法时,其他线程想使用这个方法时就必须等待,直到别的线程使用完该方法,当前线程才能使用。
使用多线程解决许多实际问题时,可能要把某些修改数据的方法用关键字synchronized来修饰。被synchronized修饰的方法,就像是临界区方法。同时只能有一个线程执行该方法。
线程常用方法
- start( ):调用该方法将启动线程,使之从初始状态进入就绪状态进行排队,一旦轮到它享受CPU资源时,就可以脱离创建它的线程独立开始自己的生命周期。
- run( ):Thread类的run( )方法与Runnable接口的run( )方法的功能和作用相同,用来定义线程对象被调度后所执行的操作。
- sleep( ):优先级别高的线程可以调用sleep方法来主动放弃CPU资源,从而休眠一段时间。
- isAlive( ):线程处于初始状态时,调用isAlive( )方法返回false。在线程的run( )方法结束之前,即没有进入死亡之前,调用该方法返回true。
- currentThread( ):该方法是Thread类的类方法,可以用类名调用,该方法返回当前正在使用CPU资源的线程。
- interrupt( ):一个占有CPU资源的线程可以让休眠线程调用interrupt( )方法"唤醒"自己,即导致休眠的线程发生InterruptedException异常,从而结束休眠,重新排队等待CPU资源。
- join():在当前线程中调用其他线程对象的join()方法时,当前线程阻塞,被调用join()方法线程执行完毕或等待超时,当前线程才会再次进入就绪状态,等待分配CPU资源。
反射、代理
Java反射
反射就是在程序运行时动态获取类的属性、方法、父类、接口等信息,还能通过反射在程序运行期间实例化对象、调用方法,即使方法或属性是私有的,也可以通过反射的形式来调用。
Java中主要由以下的类来实现Java反射机制:
- Class类:代表一个类
- Field类:代表类的成员变量
- Method类:代表类的方法
- Constructor类:代表类的构造方法
- Array类:提供动态数组的创建,以及访问数组的元素的静态方法
反射常用方法:
- getName():获得类的完整名字
- getFields():获得类的public类型的属性
- getDeclaredFields():获取类的所有属性。
- getDeclaredMethods():获取类的所有方法。
- getMethod(String name,Class[] parameterTypes):获得类的特定方法,name参数指定方法的名字,parameterTypes参数指定方法的参数类型。
- getConstrutors():获得类的public类型的构造方法。
- getConstrutor(Class[] parameterTypes):获得类的特定构造方法,parameterTypes参数指定构造方法的参数类型。
- newInstance():通过类的不带参数的构造方法实例化类对象。
问:getDeclaredMethod( )和getMethod( )的区别?
- getDeclaredMethod*( )获取的是类自身声明的所有方法,包含public,protected和private方法。
- getMethod*( )获取的是类所有的共有方法,这就包括自身的所有public方法,还有从基类继承的,从接口实现的所有public方法。
问:Class.forName和ClassLoader.loadClass()的区别?
- Class.forName()除了将类的.class文件加载到jvm中之外,还会对类进行解析,执行类中的static块。
- ClassLoader.loadClass()只干了一件事情,就是将.class文件加载到jvm中,并不会执行static中的内容,只有newInstance才会去执行static块中的内容。
注解
什么是注解
Java注解是附加在源代码中的一些元信息,用于一些工具编译、运行时的解析和使用,起到说明、配置的功能,注解不会影响代码的实际逻辑,仅仅只是起到辅助作用。
如果说注释是给程序员看的,帮助程序员人理解程序逻辑,那么注解就是给程序看的,它就像是一个标签,可以贴在类、方法或变量上。比如程序读到加了@Test注解的方法,就知道该方法是个测试方法,又或者读到使用@Before注解的方法,程序就知道该方法要比一般方法先执行。
Java的JDK中默认给提供了一些注解,比如使用@Override标记该方法需要检验父类方法是否可重写,使用@Deprecated标记该方法已经过时等。
注解的分类
注解大致可被分为三类:JDK内置注解、第三方框架注解、自定义注解。
JDK内置一些注解,共7个,3个在java.lang中,4个在java.lang.annotation中。
3个常用注解:
- @Override:表示当前方法是一个重写方法,在编译阶段对方法进行检测
- @Deprecated:表示当前方法是一个已经过时的方法
- @SuppressWarnings:表示忽略程序中的警告
4个元注解:指用来修饰注解的注解,在自定义注解时使用。
- @Retention:用来定义注解的保留策略(SOURCE/ClASS/RUNTIME)
- RetentionPolicy.SOURCE:注解只在源码阶段保留,编译器进行编译时它将被丢弃忽视
- RetentionPolicy.CLASS:注解只被保留到编译进行时,它并不会被加载到JVM中
- RetentionPolicy.RUNTIME:注解可以保留到程序运行时,它会被加载进入到 JVM 中,在程序运行时可获取到
- @Target:用来定义注解的使用位置,默认是各个位置都可以使用
- @Document:用于制作文档,将注解中的元素包含到Javadoc中去,不是很重要
- @Inherited:用作定义被修饰的对象被继承时注解是否会被自动继承
- 类继承关系中、子类会继承父类使用的注解中被@Inherited修饰的注解
- 接口继承关系中、子接口不会继承父接口中的任何注解,不管注解是否使用@Inherited修饰
- 类实现接口关系中、实现类不会继承任何接口中定义的注解,不管注解是否使用@Inherited修饰
- @Repeatable:JDK1.8加入的,用来定义注解可以同时作用一个对象多次,但每次作用注解又可以代表不同的含义
第三方框架:一般的第三方框架都会为开发者准备一些注解来方便开发,比如SpringMVC中的@Controller等。
自定义注解:除了使用内置的注解或第三方框架准备的注解,我们还可以定义属于自己的注解。
定义格式:
public @interface 注解名称{
属性列表;
}
示例:
public @interface Student{
String name(); // 属性没有默认值,使用时必须指定值。
int age() default 0 ; // 有默认值,使用时,可以不给,也可以给。
String[] hobbies(); // 数组格式,按照数组格式赋值,只有一个时,可以省略大括号。
}
//使用
@Student(name="Bilili",hobbies={"开发","逆向"}) // 如果注解的属性只有一个,且叫value,那使用该注解时可不用指定属性名。
public void abc(){
sout("注解使用");
}
注解的解析
需要注意,程序只要用到注解,那么必然就会涉及3个操作:定义注解、使用注解、读取注解。如果只是完成了前面两步,一般没什么用,还需要编写读取这些注解的逻辑,然后让程序解析这些注解去完成相应的功能。
一般获取注解的方法可以通过读取文件获取,也可以通过反射获取。不过在获取注解时需要注意定义注解时使用的保留策略,也就是前面提到的元注释中@Retention的设置。如果想要通过反射获取的话就需要将保留策略设置成RetentionPolicy.RUNTIME,这样注解才会被保留到程序运行时。除了反射获取还可以通过读取文件来获取,比如@Override注解,它是由编译器读取的,编译器会读取用这个注解标记过的代码,并进行语法检查。通过源码可知@Override注解的保留策略是RetentionPolicy.SOURCE,也就是只有源码阶段才会有这个注解,编译成class文件后该注解就会被丢弃忽视。
package java.lang;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
获取注解的相关方法:
getAnnotations():获取所有注解。
getAnnotation(Class annotationClass):获取指定注解,传递的那个注解的Class,获取的就是谁的注解。
isAnnotationPresent(Class annotationClass):判断指定注解是否存在,参数是注解的Class。