CSDN仅用于增加百度收录权重,排版未优化,日常不维护。请访问:www.hceng.cn 查看、评论。
本博文对应地址: https://hceng.cn/2018/09/12/JAVA学习笔记/#more
毕业后就没怎么碰Java了,为了后面Android的学习,将Java的基础知识整理一下。
0. 开发环境的搭建
在后面Java学习的过程中,要敲写示例代码在Linux上测试,因此需要先用虚拟机安装一个Linux发行版和Java。
准备使用的Linux发行版为Ubuntu18.04.1
,考虑到后期Android学习也会在该虚拟机上,因此在安装过程就得先设置好硬盘大小和交换分区,感觉还是有必要记录一下。
0.1 为Ubuntu设置两个分区
首先是安装虚拟机VMware Workstation Pro
和下载Ubuntu 18.04.1 LTS
就不废话了。
然后新建虚拟机,选择“自定义(高级)”,再默认“下一步”,然后选择下载好的Ubuntu镜像文件ubuntu-18.04.1-desktop-amd64.iso
,设置名字、密码等,再设置虚拟机文件放置位置(选择一个剩余空间大于100G的盘),选择核心数,设置内存,设置为“使用桥接网络”,默认IO控制器类型,默认磁盘类型,再选择“创建新虚拟磁盘”,磁盘大小为20G(作为root分区,存放Ubuntu自身系统等),磁盘文件名也改为“root.vmdk”,然后取消“创建后开启虚拟机”再点击“完成”,此时再选择“编辑虚拟机设置”,选中“CD/DVD (SATA) 正在使用文件autoinst.iso”,将其移除,再点击“添加”,选择“硬盘”,再一直点默认“下一步”,直到设置磁盘大小界面,设置为100G(作为work分区,存放后面的Android源码等),名字也改为“work.vmdk”,完成后,即可“开启此虚拟机”。
- 这里解释下为什么这样繁琐的设置两个分区。
假如只有一个分区,在以后的使用中,假如Ubuntu不小心被损坏,不能进入系统,那么系统里保存的数据就无法获取,资料也就没了。
假如分成了两个分区,一个作为系统分区,一个作为工作数据分区,即使Ubuntu系统崩溃,只需重新安装一个Ubuntu,挂接原来的工作数据分区即可,所有的文件仍然会保留。此外,扩展分区大小,也相对比较方便。
0.2 为Ubuntu设置交换分区
首次启动虚拟机后,进入系统配置界面,选择默认语言“English”,点击“Install Ubuntu”,选择默认键盘布局,选择最小安装“Minimal installation”,去掉“Download update while installing Ubuntu”,不然要安装很久,然后点击“Continue”,选择“Someting else”,选中“/dev/sda”(20G的root分区),右键点击“New Partition Table……”,在弹出的窗口选择“Continue”,再选中新出现的“free space”,右键点击“Add……”,在“Use as:”选项卡里选择“swap area”,大小改为10240(Android编译要求16G的内存+交换分区,内存不够交换分区凑),完成后再点击该“free space”,把剩下的大小作为“Ext4 journaling file system”,“Mount point:”设置为根目录“/”,然后以类似的方式设置“/dev/sdb”,全部空间作为“Ext4 journaling file system”,“Mount point:”设置为手动编辑的“/work”,分区完成效果如下:
最后点击“Install Now”,选择时区为“Shanghai”,设置主机名字、密码等,再根据提示重启。
0.3 为Ubuntu安装基本软件
前面的博客Linux开发环境配置及shell script对嵌入式开发所需使用到的软件有过一次分析,另外博客搭建嵌入式Linux开发环境有如何安装和配置,参考博客,完成对ftp、ssh、nfs、smaba、vim的安装和配置,这些就不废话了,目前只是学习Java,至于其它的软件,后面需要再安装。
接下来就是安装本次的最主要的Java:
sudo apt-get install openjdk-8-jdk
这里只安装了JDK,因为JDK包含了JRE:
- JDK (Java Development Kit):JAVA开发环境,包含JRE;
- JRE (Java Runtime Environment):JAVA运行环境,包含虚拟机但不包含编译器;
以上就完成了Java学习环境的准备。
1. Java基础
1.1 第一个Java程序
{% codeblock lang:java [Hello.java] %}
public class Hello {
public static void main(String args[]) {
System.out.println(“Hello world!”);
}
}
{% endcodeblock %}
编译、执行:
javac Hello.java //编译
java Hello //执行
这里Java文件名首字母大写是因为:
在Java编程规范里,类的首字母要大写,而类的名字要和文件名一致;
1.2 数据类型
Java的数据类型分为8个基本数据类型和3个引用数据类型。
1.2.1 基本数据类型
序号 | 数据类型 | 关键字 | 占用比特数/字节数 | 取值范围 | 缺省数值 |
---|---|---|---|---|---|
1 | 布尔型 | boolean | 8/1 | true,false | false |
2 | 字节型 | byte | 8/1 | -128~127 | 0 |
3 | 字符型 | char | 16/2 | 0~65535 | ‘\u0’ |
4 | 短整数 | short | 16/2 | -32768~32767 | 0 |
5 | 整型 | int | 32/4 | -2147483648~2147483647 | 0 |
6 | 长整型 | long | 64/8 | -9.22E-45~9.22E+18 | 0 |
7 | 单精度浮点型 | float | 32/4 | 1.4013E-45~3.4028E+38 | 0.0F |
8 | 双精度浮点型 | double | 64/8 | 2.22551E-208~1.7977E+308 | 0.0D |
Java中所有的基本数据类型都有固定的存储范围和所占内存空间的大小,而不受具体操作系统的影响,来保证Java程序的可移植性。
整形数据默认为int数据类型,浮点型默认为double数据类型,如果要表示long型数据或float型数据,要在相应的数值后面加上l、L或f、F,否则会出现编译问题。
1.2.2 引用数据类型
Java的引用数据类型包括:数组(array)、类(class)、接口(interface),其缺省值都为null。
基本数据类型的变量名指向具体的数值,而引用数据类型的变量名指向存数据对象的内存地址,有点类型C语言的指针。
当引用指向null时,Java会自动释放对应的空间。
1.2.3 数据转换
Java在不丢失数据的前提下,可以实现自动转换,比如int型转换为long型,即由小范围变大范围可以,大范围变小范围不行。
-
示例:
{% codeblock lang:java [Var.java] %}
public class Var {
public static void main(String args[]) {
//基本数据类型:变量名指向具体的数值(数据在栈)
boolean a = true; //布尔型
byte b = 1; //字节型
char c = ‘h’; //字符型
short d = 2; //短整数
int e = 3; //整型
long f = 4L; //长整型
float g = 3.14F; //单精度浮点型
double h = 3.14D; //双精度浮点型//引用数据类型:变量名指向存数据对象的内存地址(引用在栈,数据在堆)
int p1[] = new int[10]; //分配整数数组,类似C语言int p1[10];或int* p1 = malloc(10*sizeof(int));
int p2[] = {1, 2, 3}; //分配并定义,类似C语言int p2[10] = {1, 2, 3};String str = "hceng"; //类引用,分配并定义,类似C语言char str[] = "hceng"; p1 = null; //自动释放 p2 = null; str = null;
//数据转换
int a1 = 30;
long b1 = a1; //自动转换,因为long的表示范围比int大
float c1 = 3.1f;
int d1 = (int)c1; //必须使用强制转换,因为数据会丢失short s = 1; //s = s + 1; //出错,因为s+1自动将s变成了int型与1相加,再赋值给short型的s,大范围变小范围不行 //s = s + s; //出错,因为s+s也会自动变成int型赋值给short型的s,大范围变小范围不行 s = (short)(s + 1); s = (short)(s + s);
}
}
{% endcodeblock %}
1.3 运算符、表达式、语句
Java的分支if, if...else, if...else if...else, switch
和循环while, do...while, for; break, continue
都和C语言是一样的。
1.4 方法
在C语言里函数的的叫法,在Java中没有了,类似的东西叫方法,所谓的方法就是用来解决一类问题的代码的有序组合,是一个功能模块。
1.4.1 方法的格式
public static 返回值类型 方法名称 (类型 参数1, 类型 参数2, ...) {
程序语句;
[return 表达式];
}
- 示例:
{% codeblock lang:java %}
public static int add(int x, int y) {
int sum;
sum = x + y;
return sum;
}
{% endcodeblock %}
1.4.2 方法的重载
即方法名相同,参数类型或个数不同,返回值也可相同也可以不同,就会调用到不同的方法。
-
示例:
{% codeblock lang:java [Method.java] %}
public class Method {
public static void main(String args[]) {
System.out.println(add(1, 2));
System.out.println(add(1, 2, 3));
System.out.println(add(1.0f, 2.0f));
}public static int add (int x, int y) {
return x + y;
}public static int add (int x, int y, int z) {
return x + y + z;
}public static float add (float x, float y) {
return x + y;
}
}
{% endcodeblock %} -
结果:
3
6
3.0
三个方法名字都一样,第一个和第二个参数个数不同,第一个和第三个参数类型不同,在调用方法时,传入的参数和哪一个匹配,就会调用到哪一个方法。
1.4.3 方法的参数
-
示例:
{% codeblock lang:java [Param.java] %}
public class Param {
public static void main(String args[]) {
int x = 1;
System.out.println(“Before x=” + x);
fun(x);
System.out.println(“After x=” + x);int p[] = new int[1]; p[0] = 2; System.out.println("Before p=" + p[0]); fun(p); System.out.println("After p=" + p[0]);
}
public static void fun(int x) {
x = 100;
}public static void fun(int[] p) {
p[0] = 200;
}
}
{% endcodeblock %} -
结果:
Before x=1
After x=1
Before p=2
After p=200
与C语言类型,如果传入的为形参,在方法里修改变量值,外部的方法的参数不会被改变,可以通过引用数组(类型C语言的指针)的方式,实现对传入参数的修改。
基本数据作为参数,方法内部对参数的修改不影响调用者;
引用数据作为参数,方法内部修改了堆,结果会保留下来;
2. Java面向对象编程
在Java中,方法类似C语言中函数,类类似C语言中结构体。
Java面向对象编程有三大特性:封装性、继承性、多态性。
2.1 类的引入
**在Java面向对象的思维里,把拥有共同特征的事物抽象出来叫做类,把符合这个类特征的个体叫做对象。**比如“人”就是类,具体的某个人“张三”就是对象,类的共同特征包含“人的名字”、“人的年龄”等。
如下面例子,定义了一个类Person
,在main
里,通过new
创建具体的对象,再通过对象调用成员方法,或者直接访问类方法或类变量。
-
示例:
{% codeblock lang:java [Oop.java] %}
//类的定义(类是创建对象的模板,创建对象也叫类的实例化)
class Person {
//类变量(调用不需要实例化对象)
static int count;//类方法(调用不需要实例化对象)
static void printPerson () {
System.out.println(“This is a class of Person”);
}//静态代码块(只会被调用一次,且先于构造代码块、构造方法执行)
static {
System.out.println(“Execute only once”);
}//构造代码块(每次创建对象时都会被调用,且先于构造方法执行)
{
System.out.println(“Every call is executed”);
count ++;
}//类的成员
String name; //成员变量
int age; //成员变量
//成员方法
String getName() {
return name;
}//构造方法(没有返回值,方法名必须和类名一样)
//实现new时传入参数,重载实现传入的参数多样化
public Person () {
name = “null”;
age = 0;
}public Person (String name) {
this.name = name; //name是局部变量
age = 0;
}public Person (String name, int age) {
this.name = name;
this.age = age;
}
}
//一个源文件中只能有一个public类,且与文件名相同
public class Oop {
public static void main(String args[]) {
Person p0 = new Person(); //创建对象,即类的实例化
Person p1 = new Person(“hceng”);
Person p2 = new Person(“hceng, 23”);
System.out.println(p0.getName()); //调用成员方法
System.out.println(p1.getName());
System.out.println(p2.getName());
Person.printPerson(); //访问类方法
System.out.println("Person number is " + Person.count); //访问类变量
}
}
{% endcodeblock %}
- 结果:
Execute only once
Every call is executed
Every call is executed
Every call is executed
null
hceng
hceng, 23
This is a class of Person
Person number is 3
对该示例进行分析,涉及了好几个知识点。
Person
通过class
关键词定义为类;
2.1 通过static
修饰的变量叫类变量,可以不通过new
创建对象进行访问;
2.2 通过static
修饰的方法叫类方法,可以不通过new
创建对象进行访问;
3.1 通过static { }
修饰的代码块叫静态代码块,在类被创建的时候调用,且只会被调用一次,先于构造代码块、构造方法执行;
3.2 通过{ }
修饰的代码块叫构造代码块,在每次类被创建的时候调用,先于构造方法执行;
4.1 接下来是类的成员,包含成员变量和成员方法;
4.2 成员方法:实现对类中成员变量的操作,提供某些功能,成员方法通过对象调用;
5.1 接下来是构造方法,没有返回值,方法名必须和类名一样,如果没实现构造方法,编译器自动加上一个无参数的空构造方法;
5.2 **构造方法:**用于创建类的实例并对实例的成员变量进行初始化,构造方法通过new运算符调用;
5.3 这里利用重载实现了传入参数的多样化,this
关键字表示当前类;
6.1 接下来是public
修饰的类,也是唯一的一个,和文件名相同,里面有main
成员方法;
6.2 在main
里,首先创建了三个对象,每个对象传入的参数不同,调用的构造方法也会不同;
6.3 然后调用对象的成员方法;
6.4 最后直接对类变量、类方法进行访问;
2.2 封装性
在Java中,将属性(变量)和方法封装成一个整体(也就是类),就是封装性的体现。
对于这个整体,里面有些属性外部可以直接访问,有些可能期望按要求访问,对于特殊的属性,可以先设置为私有的权限,再通过属性的方法进行访问,就属性的方法里就是我们期望访问的方式。
就比如下例中的年龄,我们期望年龄为非负的值,假如外部直接修改年龄就有被设置为负的风险。现在将年龄私有化,使外部无法访问,只能通过公共的类的方法进行访问,这个方法里就对传入的参数进行判断纠正,从而满足年龄非负的要求。
-
示例:
{% codeblock lang:java [Enc.java] %}
class Person {
/* 成员变量(属性) */
//int age; //default
private int age; //私有,只能供类内部访问/* 成员方法 */
public void setAge(int age) {
if (age < 0 || age > 200)
age = 0;
else
this.age = age;
}public int getAge() {
return age;
}
}
public class Enc {
public static void main(String args[]) {
Person per = new Person();
//per.age = -1; //外部直接访问,不好控制
per.setAge(-1);
System.out.println(per.getAge());
}
}
{% endcodeblock %}
- 结果:
0
Java中四种权限:
private: 被其修饰的属性以及方法只能被该类的对象访问,其子类不能访问,更不能允许跨包访问;
**default:**默认访问权限,只允许在同一个包中进行访问;
**protected:**被其修饰的属性以及方法只能被类本身的方法及子类访问,即使子类在不同的包中也可以访问;
public: 被其修饰的类、属性以及方法不仅可以跨类访问,而且允许跨包访问;
权限 | 类内 | 同包 | 不同包子类 | 不同包非子类 |
---|---|---|---|---|
private | √ | × | × | × |
default | √ | √ | × | × |
protected | √ | √ | √ | × |
public | √ | √ | √ | √ |
2.3 继承性
2.3.1 引入
为了实现代码的复用,Java中引入了继承性。
如下例子,子类Student
继承了父类Person
,就可以访问父类的属性(非私有)和方法(非私有)。
-
示例:
{% codeblock lang:java [Ext.java] %}
class Person {
//变量/属性
private int age;//方法
public void setAge(int age) {
if (age < 0 || age > 200)
age = 0;
else
this.age = age;
}public int getAge() {
return age;
}public void printfInfo() {
System.out.println("age = " + age);
}final public void testFinal() {
}
//构造方法
public Person() { //系统默认的构造函数,如果没有,编译器会自动生成
System.out.println(“for test Person() call”);
}public Person(int age) {
this.age = age;
System.out.println(“for test Person(int age) call”);
}
}
//Student从父类(Person)继承
class Student extends Person {
private String school;
public void setSchool(String school) {
this.school = school;
}
public String getSchool() {
return this.school;
}
//覆写(方法名在父类中有同名方法)
public void printfInfo() {
System.out.println("school = " + school);
}
//父类方法用了final修饰,子类无法覆写
//public void testFinal() {
//
//}
//构造方法
public Student() { //系统默认的构造函数,如果没有,编译器会自动生成
//super(); //调用父类的构造函数(无参数),默认调用,可不写
super(15); //调用父类的构造函数(有参数)
super.printfInfo(); //super就指代的父类
System.out.println("for test Student() call");
}
}
public class Ext {
public static void main(String args[]) {
Student per = new Student();
per.setAge(10); //方法来源于父类
System.out.println(per.getAge());
per.setSchool("ShenZhen"); //方法来源于自己扩展
System.out.println(per.getSchool());
per.printfInfo(); //覆写的父类方法
}
}
{% endcodeblock %}
- 结果:
for test Person(int age) call
age = 15
for test Student() call
10
ShenZhen
school = ShenZhen
对该示例进行分析:
1.1 定义了一个类
Person
,包含一个变量(私有)、四个方法、两个构造方法;
1.2 第四个构造方法使用了final
修饰,后面子类将不能对其覆写;
1.3 两个构造方法,一个不带参数,一个带参数;
2.1 子类Student
从父类Person
通过关键词extends
继承,包含一个新变量(私有)、三个方法、一个构造方法;;
2.2 方法printfInfo
和父类的方法printfInfo
名字一样,父类被覆写,调用printfInfo
会调用子类的printfInfo
;
2.3 方法testFinal
被注释,因为父类使用final
修饰了同名的方法,子类不能再覆写;
2.4 子类的构造方法被调用时,会先默认调用父类的构造方法,即用super
表示,如果super
带参数,表示调用父类带参数的构造方法,同时还可以通过super
访问父类的属性(非私有)和方法(非私有);
3.1 在main
里,首先new
实例化了一个Student
类;
3.2 调用Student
类的setAge
方法,这个方法是从父类继承过来的;
3.3 调用Student
类的setSchool
方法,这个方法是子类自己扩展的;
3.4 调用Student
类的printfInfo
方法,这个方法父类和子类都有,子类覆写父类的方法;
2.3.2 继承的限制
前面引入了继承,子类从父类继承过来,也就拥有了父类的一些特性,但继承也是有限制的:
-
父类的私有属性不能被子类访问;
-
父类的私有方法不能被子类访问;
-
子类覆写的方法不能缩小权限,即父类
public
,子类不能private
; -
示例:
{% codeblock lang:java [Limit.java] %}
class Father {
private int money;public void setMoney(int money) {
this.money = money;
}public void printfInfo1() {
System.out.println(“This is Father printfInfo1()”);
}private void printfInfo2() {
System.out.println(“This is Father printfInfo2()”);
}
}
class Son extends Father {
//private void printfInfo1() {
//System.out.println(“This is Son printfInfo1()”);
//}
public void printfInfo2() {
System.out.println("This is Son printfInfo2()");
}
}
public class Limit {
public static void main(String args[]) {
Son son = new Son();
//son.money = 100;
son.setMoney(100);
son.printfInfo1();
son.printfInfo2();
}
}
{% endcodeblock %}
- 结果:
This is Father printfInfo1()
This is Son printfInfo2()
对该示例进行分析:
1 定义了一个类
Father
,包含一个变量(私有)、三个方法(其中一个私有);
2.1 子类Son
从父类Father
继承,只有一个方法;
2.2 方法printfInfo1
被private
修饰,表示私有,而父类同名方法是公共的,不能覆写,无法编译通过;
2.3 方法printfInfo2
被public
修饰,表示公共,父类也有个同名方法,但却是私有的,因此这里不是覆写,而是定义的新方法;
3.1main
里面实例化了一个类Son
;
3.2Son
从父类Father
继承过来,父类的私有变量不能直接访问,只能通过父类提供的公共方法来访问;
3.3 父类的printfInfo1
是公共的,子类的printfInfo1
也只能是公共的;
3.4 父类的printfInfo2
是私有的,子类无法访问,子类的printfInfo2
是新定义的;
2.3.3 抽象类
**抽象类作用:**规定子类必须实现的方法,起“模板”作用;
- 示例:
{% codeblock lang:java [Abstract.java] %}
//抽象类
abstract class Father {
public abstract void study(); //规定了子类必须实现的方法
}
//子类
class Son extends Father {
public void study() { //实现抽象类定义的方法
System.out.println(“Son study”);
}
}
public class Abstract {
public static void main(String args[]) {
//Father father = new Father(); //抽象类不能实例化对象
Son son = new Son();
son.study();
}
}
{% endcodeblock %}
- 结果:
Son study
对该示例进行分析:
1 通过
abstract
关键字定义了一个抽象类Father
,里面有一个abstract
修饰的方法,但没有具体的实现内容;
2 子类Son
从父类Father
继承,实现了父类的方法;
3main
里不能对抽象类进行实例化,只能对抽象类的子类进行实例化,访问其方法
2.3.4 接口
**接口作用:**跟抽象类相似,起“模板”作用;子类可以继承多个接口,突破“单继承”的限制;
- 示例:
{% codeblock lang:java [Interface.java] %}
//接口
interface A {
public static final int i = 10; //接口只能定义常量
public abstract void printNum();
}
interface B {
public static final String name = “hceng”;
public abstract void printString();
}
//子类
class Son implements A,B {
public void printNum() { //实现接口定义的方法
System.out.println("Num = " + i);
}
public void printString() {
System.out.println("String = " + name);
}
}
public class Interface {
public static void main(String args[]) {
Son son = new Son();
son.printNum();
son.printString();
}
}
{% endcodeblock %}
- 结果:
Num = 10
String = hceng
对该示例进行分析:
1.1 通过
interface
关键字定义两个接口A
和B
,里面定义了常量和抽象方法;
1.2 接口里面只能定义常量,抽象类可以定义常量和变量;
2.1 子类Son
同时从接口A
和B
继承,突破了抽象类突破“单继承”的限制;
2.2 子类Son
依旧得实现接口里面所有的抽象方法;
3main
实例化Son
,访问实现的方法;
2.4 多态性
多态性体现在方法和对象上。
在前面的例子中,方法的多态性已经接触过了,体现在方法的重载与覆写。
- **方法的重载(overload)?*定义多个同名方法,其参数类型、个数不同;
- **方法的覆写(override)?*子类里实现跟父类同样的方法,覆盖掉父类;
对象的多态性就是父对象和子对象之前的转换,转换分为向上转换和向下转换。
- 向上转型:子对象向父对象转型的过程,例如猫类转换为动物类(小范围转大范围自动),子对象独有的成员将不可访问(只能识别父对象中的内容);
- 向下转型:父对象强制转换为子对象的过程,例如动物类强制转换为猫类(大范围转小范围强制),;
另外,可以通过引用变量 instanceof 类名
的方式来判断引用变量所指向的对象是否属于某个类;
-
示例:
{% codeblock lang:java [Limit.java] %}
class Father {
public void printfInfo() {
System.out.println(“This is Father”);
}public void work() {
System.out.println(“Father is working……”);
}
}
class Son extends Father {
public void printfInfo() {
System.out.println(“This is Son”);
}
public void palyGame() {
System.out.println("Son is playing games……");
}
}
class Daughter extends Father {
public void printfInfo() {
System.out.println(“This is Daughter”);
}
public void dance() {
System.out.println("Daughter is dancing……");
}
}
public class Cnv {
public static void main(String args[]) {
Father father1 = new Father();
Son son = new Son();
father1 = son; //向上转换
father1.printfInfo();
//father1.palyGame(); //只能调用被子类覆写的方法,不能调用只在子类中定义的方法
Father father2 = new Daughter(); //创建子类的实例化对象(先向上转换)
Daughter daughter = (Daughter)father2; //向下转换 (Daughter daughter = new Daughter();)
daughter.printfInfo();
System.out.println("--------------------");
Father f = new Father();
Son s = new Son();
Daughter d = new Daughter();
//向上转换 示例
printf(f);
printf(s);
printf(d);
//向下转换 示例
printAction(f);
printAction(s);
printAction(d);
}
public static void printf(Father f) {
f.printfInfo();
}
public static void printAction(Father f) {
if (f instanceof Son) {
Son s = (Son)f;
s.palyGame();
}
else if (f instanceof Daughter) {
Daughter d = (Daughter)f;
d.dance();
}
else if (f instanceof Father) { //Father要在最后,每个对象都属于Father
f.work();
}
}
}
{% endcodeblock %}
- 结果:
This is Son
This is Daughter
--------------------
This is Father
This is Son
This is Daughter
Father is working……
Son is playing games……
Daughter is dancing……
对该示例进行分析:
1.1 首先定义了三个类,其中
Son
和Daughter
继承于Father
;
1.2 每个类里面都有一个同名的printfInfo
方法和各自的独有的方法;
2.1main
方法里,首先创建了father1
和son
这两个对象,然后将子类赋值给父类,进行向上转换,父类只能调用被子类覆写的方法printfInfo
,最后结果也是调用的子类覆写的方法;
2.2 首先创建子类的实例化对象father2
(即先向上转换,不然后面无法向下转换),此时father2
的类型是Father
,再用Daughter
强制向下转换,得到daughter
,此时可以调用子类的所有方法;
3.1 先分别实例化了三个类对应的对象;
3.2 调用方法printf
,传入不同类的对象,自动向上转换,调用各自同名的方法;
3.3 调用方法printAction
,传入不同类的对象,自动向上转换,再判断是对象是否属于对应类,再进行向下强制转换,从而调用其私有的方法(因为s
和d
都属于f
,所以对f
的判断要放在最后);
2.5 异常
程序运行时,发生的不被期望的事件,它阻止了程序按照程序员的预期正常执行,这就是异常。
异常发生时,是任程序自生自灭,立刻退出终止,还是输出错误给用户?或者用C语言风格:用函数返回值作为执行状态?。
Java提供了更加优秀的解决办法:异常处理机制。
异常处理机制能让程序在异常发生时,按照代码的预先设定的异常处理逻辑,针对性地处理异常,让程序尽最大可能恢复正常并继续执行,且保持代码的清晰。
Java标准库内建了一些通用的异常,这些类以Throwable
为顶层父类,Throwable
又派生出Error
类和Exception
类。
- Error(错误):
Error
类以及他的子类的实例,代表了JVM本身的错误,不能被程序员通过代码处理,所以不用管; - Exception(异常):
Exception
以及他的子类,代表程序运行时发送的各种不期望发生的事件,可以被Java异常处理机制使用;
针对Javac对异常的处理要求,将异常类分为2类:
- checked exception(可查异常):
IOException
以及它子类的异常,这些异常必须处理,否则编译不会通过;- unckecked exception(不可查异常):
RuntimeException
以及它的子类,不要求必须处理,发生异常时程序退出;
针对要处理的异常,可以自己处理,也可也通过throws
抛出异常,让别人处理:
- 示例:
{% codeblock lang:java [Div.java] %}
/* 除法运算: java Div 6 2 -> 6/2=3*/
public class Div {
public static void main(String args[]) {
int m, n, r;
m=n=r=0; //初始化
try {
m = Integer.parseInt(args[0]);
n = Integer.parseInt(args[1]);
r = div(m, n);
craetException();
} catch (NumberFormatException exception1) { //自己处理:数字格式异常(java Div 6 a)
System.out.println("====An exception occurs1:"+exception1+"=====");
} catch (ArithmeticException exception2) { //处理扔过来的异常:除零错误异常(java Div 6 0)
System.out.println("====An exception occurs2:"+exception2+"=====");
} catch (RuntimeException runtimeexception) { //处理所有不可查异常,比如位数不够(java Div 6)
System.out.println("====An exception occurs:"+runtimeexception+"=====");
} catch (Exception exception) { //处理自己通过throw创建的可查异常
System.out.println("====An exception occurs:"+exception+"=====");
}
finally {
//无论是否发生异常,finally代码块中的代码总会被执行,完成清理类型等收尾善后性质的功能
System.out.println("finally"); //finally中不要包含return
}
System.out.println(m+"/"+n+"="+r);
}
//扔给调用者处理:除零错误异常(java Div 6 0)
//public static int div(int m, int n) throws ArithmeticException {
public static int div(int m, int n) { //ArithmeticException是不可查异常,出现后会自动抛出,可以不要throws
int r;
r = m / n;
return r;
}
//自己创建的可查异常NullPointerException,必须处理,这里将其抛出,因此必须要throws
public static void craetException() throws Exception {
throw new Exception("My Craeted Exception");
}
}
{% endcodeblock %}
- 结果:
hceng@android:/work/java_learn/09th_exception$ java Div 6 3
====An exception occurs:java.lang.Exception: My Craeted Exception=====
finally
6/3=2
hceng@android:/work/java_learn/09th_exception$ java Div 6 a
====An exception occurs1:java.lang.NumberFormatException: For input string: "a"=====
finally
6/0=0
hceng@android:/work/java_learn/09th_exception$ java Div 6 0
====An exception occurs2:java.lang.ArithmeticException: / by zero=====
finally
6/0=0
hceng@android:/work/java_learn/09th_exception$ java Div 6
====An exception occurs:java.lang.ArrayIndexOutOfBoundsException: 1=====
finally
6/0=0
对该示例进行分析:
1 该示例实现了一个整数除法运算,同时对输入参数的合法性进行了异常处理;
2.1main
方法里,首先定义了三个变量并初始化;
2.2try { }
代码块包含了可能出现异常的代码;
2.3 通过Integer.parseInt
方法将字符args[0]
和args[1]
转换成了整型,这里可能出现NumberFormatException
和ArrayIndexOutOfBoundsException
异常,比如传入a
就无法转换成对应数字,传入的参数少一个也会转换出错;
2.4 方法div
的定义在后面,可能出现ArithmeticException
异常,即初始不能为零;
2.5 方法craetException
的定义在后面,在里面自己产生了一个可查异常,必须处理的异常;
2.6 接下来的catch (异常类 异常变量名) { }
代码块,捕获到对应异常,即进行代码块代码;
2.7 无论是否发生异常,finally { }
代码块的内容总会被执行,一般在里面做一些清理类型的善后收尾工作,注意不要在里面写return
代码,因为如果前面的try
或catch
块中有return
或throw
语句,会先执行finally
块,此时finally
中有return
就直接返回了,无法再返回来执行try
或catch
块中return
或throw
语句里面的内容;
2.8 接下来是打印出结果;
3.1 定义了Div
方法,因为可能发生ArithmeticException
异常,该异常是不可查异常,出现后可以自动抛出,所以在方法名定义那可以不加throws ArithmeticException
;
3.2 定义了craetException
方法,该方法是通过new
建立,throw
抛出,从前面的图中可以知道Exception
类异常包含IOException
可查异常和RuntimeException
不可查异常,因为是可查异常的关系,要么自己处理,要么手动抛出,这里抛出的话,方法名定义得加上throws Exception
,否则编译都无法通过;
3.3 这里的两个方法,都是抛出异常,也可以自己通过try {} catch {}
处理掉;在选择抛出时,前者因为是不可查异常,在方法名定义时可以不加throws 异常类型
,后者反之得加上;
4 运行代码时,分别列举了正常运行、传入参数为字母错误、传入参数除数为零、传入参数少一个的情况,异常都被捕获到,程序没有当时直接退出,而是执行了异常处理代码块的内容,继续执行;
2.6 包及访问权限
为了更好地组织类,Java提供了包机制,解决类的同名冲突问题;
如同文件夹一样,包也采用了树形目录的存储方式。同一个包中的类名字是不同的,不同的包中的类的名字是可以相同的,当同时调用两个不同包中相同类名的类时,应该加上包名加以区别。因此,包可以避免名字冲突。
假设有如下情况,hceng和jack两个程序员,实现了同一个Math.java
类,实现了同名方法add
,但内容不同,源码组织情况如下:
├── hceng
│ ├── Math.java
│ └── Permission.java
├── jack
│ ├── Math.java
│ └── TestAccess.java
└── Pack.java
使用如下命令编译:
javac -d . hceng/*.java jack/*.java *.java
编译后文件组织如下:
├── hceng
│ ├── Math.java
│ └── Permission.java
├── jack
│ ├── Math.java
│ └── TestAccess.java
├── pack
│ ├── hceng
│ │ ├── Math.class
│ │ ├── packagePermission.class
│ │ └── Permission.class
│ └── jack
│ ├── Math.class
│ └── TestAccess.class
├── Pack.class
└── Pack.java
执行java Pack
效果如下:
add1:3
add2:4
add2:-1
Can only be accessed by this class: a = 1
Access in the same package: b = 2
Access in different packages: c = 3
Accessible anywhere: d = 4
源码见文末的Github链接,在10th_package
里面。
先从文件组织的角度进行分析:
1.1
hceng
路径下有个Math.java
,里面有个Math
方法和sub
方法,通过package
关键字指定了打包的路径为pack/hceng
;
1.2jack
路径下有个Math.java
,里面有个Math
方法,通过package
关键字指定了打包的路径为pack/jack
;
2.1 编译时加入了-d
参数,表示指定生成的包文件路径;
2.2 根据编译参数和类文件package
的定义,就在当前路径在生成了pack/hceng
和pack/jack
存放了对应的class
;
3.1 当前路径下有个Pack.java
,通过import
关键字将前面两个包导入了该类里面;
3.2main
里面,通过指定包的路径调用对应的方法,比如pack.jack.Math.add
和pack.hceng.Math.add
,解决调用同名类的冲突;
再从访问权限的角度进行分析:
1.1
hceng
路径下有个Permission.java
,里面有个公共的Permission
类和非公共的packagePermission
类,外部只能访问公共的Permission
类;
1.2 从类的权限角度可知:public
类可以被外包访问,非public
类只能在本包访问,另外,一个文件只能有一个public
类;
2.1hceng
路径下有个Permission.java
,在类Permission
里定义了四种权限的属性(属性加static是因为可以不通过new创建,直接访问);
2.2 针对private
的属性,只能在本类里访问,因此在本类的方法可以直接调用(方法加static是因为可以不通过new创建,直接访问);
2.3 针对default
的属性,只能在同包里访问,因此在同包的另一个方法里实现了访问;
2.4 针对protected
的属性,只能在不同包子类里访问,因此在jack/
路径下创建了TestAccess.java
,它属于pack.jack
包,里面通过继承类访问了该属性;
2.5 针对public
的属性,在任何地方都可以访问,因此在Pack
里直接进行了访问;
2.6 从属性权限角度可知:类成员的访问权限如下:
权限 | 类内 | 同包 | 不同包子类 | 不同包非子类 |
---|---|---|---|---|
private | √ | × | × | × |
default | √ | √ | × | × |
protected | √ | √ | √ | × |
public | √ | √ | √ | √ |
最后再补充一下jar
,对于前面生成的pack
目录,我们可以将其压缩打包,相关命令如下:
jar -cvf pack.jar ./pack //打包
jar -tvf pack.jar //查看包内容
jar -xvf pack.jar //解包
操作逻辑和tar
压缩一样的,很好记。
此时在当前目录生成pack.jar
,删除pack
文件夹,执行export CLASSPATH=.:pack.jar
指定运行查找包的路径,执行java Pack
仍可正常运行。
2.7 内部类
内部类,在类的内部定义一个类,通过内部这个类,可以访问到该类的私有属性。
内部类又分一般内部类、静态内部类、匿名内部类。
-
示例:
{% codeblock lang:java [Inner.java] %}
class OutClass {
//一般内部类
private int a = 1;
class InnerClass1 {
public void printInfo() {
System.out.println("a = " + a);
}
}//静态内部类
private static int b = 2;
static class InnerClass2 {
public void printInfo() {
System.out.println("b = " + b);
}
}
}
interface PrintInterface {
public void printInfo();
}
class MyInterface implements PrintInterface {
public void printInfo() {
System.out.println(“MyInterface”);
}
}
public class Inner {
public static void main(String args[]) {
OutClass o = new OutClass(); //依次定义外部类
OutClass.InnerClass1 i1 = o.new InnerClass1(); //内部类
i1.printInfo();
OutClass.InnerClass2 i2 = new OutClass.InnerClass2(); //直接定义内部类
i2.printInfo();
//正常调用继承于接口的类
MyInterface m = new MyInterface();
m.printInfo();
//匿名内部类(没有名字的内部类,必须继承一个父类或实现一个接口)
PrintInterface p = new PrintInterface() {
public void printInfo() {
System.out.println("PrintInterface");
}
};
p.printInfo();
}
}
{% endcodeblock %}
- 结果:
a = 1
b = 2
hello MyInterface
hello PrintInterface
对该示例进行分析:
1.1 定义了一个
OutClass
类,里面包含两个私有变量和两个内部类,再通过内部类的方法访问私有变量;
1.2 两者区别是后者使用static
修饰变量和类,使得可以不通过new
直接访问类方法;
2.1 定义了一个接口PrintInterface
,接口里定义了一个printInfo
方法;
2.2 定义了类MyInterface
继承于接口PrintInterface
,并实现了printInfo
方法;
3.1 在main
里,先实例化外部类,再实例化内部类,才能方法问一般内部类的方法;
3.2 针对静态内部类,可以直接定义内部类,再访问其方法;
4.1 针对继承于接口/父类的子类,一般先实例化,再访问其方法;
4.2 也可以在使用时,再定义方法内容,再访问其方法,这就是匿名内部类,一个没有名字的内部类;
3. JNI
JNI(Java Native Interface)就是JAVA本地接口,它允许Java代码和Native代码进行交互,这里的Native代码指C、C++语言等编程语言。
无论是Android还是Linux,其底层都是用C语言编写的,因此很多程序和库都也是用C、C++来写的,重复利用这些Native语言编写的库是十分有必要的,而且一般Native语言编写的库具有更好的性能。
这样就产生了一个问题,Java世界的代码要怎么使用Native世界的代码呢,这就需要一个桥梁来将它们连接在一起,而JNI就是这个桥梁。
3.1 JAVA调用C
3.1.1 Java访问C库的方法
Java访问C库的的步骤有三步:
1.Java中加载C库(System.loadLibrary);
2.建立Java函数名与C库函数名的映射关系;
3.在Java程序里调用C库的函数;
其中,建立Java函数名与C库函数名映射关系的方法有两种:隐式建立和显式建立;
- 示例:
{% codeblock lang:java [Jni.java] %}
public class Jni {
/* 1.加载 /
static { //静态代码块(只会被调用一次)
System.loadLibrary(“native”); //C语言会编译生成libnative.so,这里加载它
}
/ 2.声明 /
public native static void hello_implicit(); //声明(static静态方法,可直接调用)
public native static void hello_explicit();
/ 3.调用 */
public static void main(String args[]) {
hello_implicit();
hello_explicit();
}
}
{% endcodeblock %}
{% codeblock lang:c [native.c] %}
#include <stdio.h>
#include “Jni.h”
//隐式建立:函数名必须固定格式,参考生成的Jni.h
void Java_Jni_hello_1implicit(JNIEnv *env, jobject cls)
{
printf(“hello java, I am from C language(implicit)\n”);
}
//显式建立,函数名自定义
void c_hello(JNIEnv *env, jobject cls)
{
printf(“hello java, I am from C language(explicit)\n”);
}
static const JNINativeMethod methods[] = {
{“hello_explicit”, “()V”, (void *)c_hello}, //Java里调用的函数名;JNI字段描述符(参数、返回值);C语言实现的本地函数
};
//一旦Java调用System.loadLibrary,就会先调用JNI_OnLoad
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *jvm, void *reserved)
{
JNIEnv *env;
jclass cls;
//根据版本获得env,为后面提供函数
if ((*jvm)->GetEnv(jvm, (void **)&env, JNI_VERSION_1_4))
return JNI_ERR;
//查获取调用本程序的类
cls = (*env)->FindClass(env, "Jni");
if (cls == NULL)
return JNI_ERR;
//使用RegisterNatives将C和Java建立联系
if ((*env)->RegisterNatives(env, cls, methods, sizeof(methods)/sizeof(methods[0])) < 0)
return JNI_ERR;
return JNI_VERSION_1_4;
}
{% endcodeblock %}
编译及设置:
javac Jni.java
javah -jni Jni
gcc -I /usr/lib/jvm/java-1.8.0-openjdk-amd64/include/ -I /usr/lib/jvm/java-1.8.0-openjdk-amd64/include/linux -fPIC -shared -o libnative.so native.c
export LD_LIBRARY_PATH=.
java Jni
- 结果:
hello java, I am from C language(implicit)
hello java, I am from C language(explicit)
对该示例进行分析:
1.1 在Java里,先在静态代码块里加载对应的库;
1.2 然后声明要使用的方法,使用static
修饰的方法不用实例化;
1.3main
里调用方法;
2.1 在C语言里,先包含一个头文件,这个头文件通过javac Jni.java
、javah -jni Jni
生成,里面有根据Java声明的方法自动生成的C语言函数定义;
2.2 隐式建立:只需函数名和生成的Jni.h
一致就行,Java执行方法时就自动调用函数;
2.3 显式建立:函数名自定义,但需要创建JNI_OnLoad
方法将Java和C建立联系;
3.1.2 Java和C库传递数据
Java调用C语言,一般都要进行数据的传递,包括Java传入数据,C语言返回数据,这里对基本数据类、字符串、数据进行传递示例。
-
示例:
{% codeblock lang:java [Jni.java] %}
public class Jni {static {
System.loadLibrary(“native”);
}//基本类型数据
public native static float typeData1(int a);//字符串
public native static String typeData2(String str);//数组
public native static int[] typeData3(int[] a);public static void main(String args[]) {
int [] a = {1, 2, 3, 4};
int [] b = null;
int i;System.out.println(typeData1(4)); System.out.println(typeData2("hceng")); b = typeData3(a); for (i = 0; i < b.length; i++) System.out.print(b[i]+" "); System.out.println();
}
}
{% endcodeblock %}
{% codeblock lang:c [native.c] %}
#include <stdio.h>
#include “Jni.h”
#include <stdlib.h>
#if 0
//隐式建立
jfloat Java_Jni_typeData1(JNIEnv *env, jclass cls, jint a)
{
printf(“C: get val: %d, will return (float)%d \n”, a, a);
return (float)a;
}
jstring Java_Jni_typeData2(JNIEnv *env, jclass cls, jstring str)
{
const jbyte *cstr;
cstr = (*env)->GetStringUTFChars(env, str, NULL);
if (cstr == NULL)
return NULL;
printf("C: get str: %s, will return jack \n", cstr);
(*env)->ReleaseStringUTFChars(env, str, cstr);
return (*env)->NewStringUTF(env, "jack");
}
jintArray JNICALL Java_Jni_typeData3(JNIEnv *env, jclass cls, jintArray arr)
{
jint *carr;
jint *oarr;
jintArray rarr;
jint i, n = 0;
carr = (*env)->GetIntArrayElements(env, arr, NULL);
if (carr == NULL)
return 0;
n = (*env)->GetArrayLength(env, arr);
printf("C: get number: ");
for (i = 0; i < n; i++)
printf("%d ", carr[i]);
printf(", will return opposite number\n");
oarr = malloc(sizeof(jint) * n);
if (oarr == NULL)
{
(*env)->ReleaseIntArrayElements(env, arr, carr, 0);
return 0;
}
for (i = 0; i < n; i++)
oarr[i] = carr[n-1-i];
(*env)->ReleaseIntArrayElements(env, arr, carr, 0);
/* create jintArray */
rarr = (*env)->NewIntArray(env, n);
if (rarr == NULL)
return 0;
(*env)->SetIntArrayRegion(env, rarr, 0, n, oarr);
free(oarr);
return rarr;
}
#else
//显式建立
jfloat JNICALL c_typeData1(JNIEnv *env, jclass cls, jint a)
{
printf(“C: get val = %d, will return (float)%d \n”, a, a);
return (float)a;
}
jstring JNICALL c_typeData2(JNIEnv *env, jclass cls, jstring str)
{
const jbyte *cstr;
cstr = (*env)->GetStringUTFChars(env, str, NULL);
if (cstr == NULL)
return NULL;
printf("C: get str = %s, will return jack \n", cstr);
(*env)->ReleaseStringUTFChars(env, str, cstr);
return (*env)->NewStringUTF(env, "jack");
}
jintArray JNICALL c_typeData3(JNIEnv *env, jclass cls, jintArray arr)
{
jint *carr;
jint *oarr;
jintArray rarr;
jint i, n = 0;
carr = (*env)->GetIntArrayElements(env, arr, NULL);
if (carr == NULL)
return 0;
n = (*env)->GetArrayLength(env, arr);
printf("C: get number: ");
for (i = 0; i < n; i++)
printf("%d ", carr[i]);
printf(", will return opposite number\n");
oarr = malloc(sizeof(jint) * n);
if (oarr == NULL)
{
(*env)->ReleaseIntArrayElements(env, arr, carr, 0);
return 0;
}
for (i = 0; i < n; i++)
oarr[i] = carr[n-1-i];
(*env)->ReleaseIntArrayElements(env, arr, carr, 0);
/* create jintArray */
rarr = (*env)->NewIntArray(env, n);
if (rarr == NULL)
return 0;
(*env)->SetIntArrayRegion(env, rarr, 0, n, oarr);
free(oarr);
return rarr;
}
static const JNINativeMethod methods[] = {
{“typeData1”, “(I)F”, (void *)c_typeData1},
{“typeData2”, “(Ljava/lang/String;)Ljava/lang/String;”, (void *)c_typeData2},
{“typeData3”, “([I)[I”, (void *)c_typeData3},
};
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *jvm, void *reserved)
{
JNIEnv *env;
jclass cls;
if ((*jvm)->GetEnv(jvm, (void **)&env, JNI_VERSION_1_4))
return JNI_ERR;
cls = (*env)->FindClass(env, "Jni");
if (cls == NULL)
return JNI_ERR;
if ((*env)->RegisterNatives(env, cls, methods, sizeof(methods)/sizeof(methods[0])) < 0)
return JNI_ERR;
return JNI_VERSION_1_4;
}
#endif
{% endcodeblock %}
- 结果:
C: get val: 4, will return (float)4
4.0
C: get str: hceng, will return jack
jack
C: get number: 1 2 3 4 , will return opposite number
4 3 2 1
对该示例进行分析:
1.1 在Java中,依次声明了以基本数据类型、字符串、数组为参数和返回值的方法;
1.2 然后调用方法,传入参数,打印返回结果;
2.1 C语言里,分别隐式建立和显式建立编写了函数,两者主要区别在函数名和关系的建立上;
2.2 以显式建立为例,函数c_typeData1
接收Java传入的基本数据类型,直接处理,返回相应数据;
2.3 函数c_typeData2
接收Java传入的字符串,不能直接处理,使用GetStringUTFChars()
获取字符串指针,用完后使用ReleaseStringUTFChars()
释放,使用NewStringUTF()
返回字符串;
2.4 函数c_typeData3
接收Java传入的数据,不能直接处理,使用GetIntArrayElements()
获取数据,GetArrayLength()
获得数组长度,用完后使用ReleaseIntArrayElements()
释放,使用NewIntArray()
创建JNI数组,通过SetIntArrayRegion()
将数组保存到JNI数组;
2.5 修改methods[]
,使Java的方法和C语言的函数对应,以及输入输出参数(参考生成的Jni.h);
3.2 C调用JAVA
C语言调用Java相对简单一点,流程大致如下:
1.创建一个Java虚拟机
2.找到要调用的类;
3.获取/设置属性(非必须):
3.1获取属性ID;
3.2获取/设置属性;
4.对于静态方法不需要示例化对象:
4.1获取方法ID;
4.2准备传入参数(非必需);
4.3调用方法;
5.对于非静态方法需要实例化对象:
5.1获取构造方法()ID;
5.2创建对象;
5.3获取方法ID;
5.4准备传入参数(非必需);
5.5调用方法;
6.销毁创建的Java虚拟机:
-
示例:
{% codeblock lang:java [Hello.java] %}
public class Hello {
private static String name;
private static int age;public static void main(String args[]) { //静态方法
System.out.println("java: name: "+ name + ", age: " + age);
}public int typeData(String str) { //非静态方法
System.out.println("java: get str: “+ str + " ,will return 100”);
return 100;
}
}
{% endcodeblock %}
{% codeblock lang:c [caller.c] %}
#include <stdio.h>
#include <jni.h>
/* create java virtual machine*/
jint create_vm(JavaVM** jvm, JNIEnv** env)
{
JavaVMInitArgs args;
JavaVMOption options[1];
args.version = JNI_VERSION_1_6;
args.nOptions = 1;
options[0].optionString = “-Djava.class.path=./”;
args.options = options;
args.ignoreUnrecognized = JNI_FALSE;
return JNI_CreateJavaVM(jvm, (void **)env, &args);
}
int main(int argc, char **argv)
{
int r;
int ret = 0;
JavaVM* jvm;
JNIEnv* env;
jclass cls;
jfieldID nameID, ageID;
jmethodID mid, cid;
jobject jobj;
jstring jstr;
/* 1. create java virtual machine */
if (create_vm(&jvm, &env)) {
printf("can not create jvm\n");
return -1;
}
/* 2. get class */
cls = (*env)->FindClass(env, "Hello");
if (cls == NULL) {
printf("can not find hello class\n");
ret = -1;
goto destroy;
}
/----------------Non-generic part------------------/
/* 3.get/set field */
// 3.1 get field id (GetFieldID, GetStaticFieldID)
// 3.2 get/set field (Get<Type>Field,GetStatic<Type>Field / Set<Type>Field,SetStatic<Type>Field)
//java: private static String name;
nameID = (*env)->GetStaticFieldID(env, cls, "name", "Ljava/lang/String;"); //3.1
if (nameID == NULL) {
ret = -1;
printf("can not get field name\n"); //3.2
goto destroy;
}
jstr = (*env)->NewStringUTF(env, "hceng");
(*env)->SetStaticObjectField(env, jobj, nameID, jstr);
//java: private static int age;
ageID = (*env)->GetStaticFieldID(env, cls, "age", "I"); //3.1
if (ageID == NULL) {
ret = -1;
printf("can not get field age\n");
goto destroy;
}
(*env)->SetStaticIntField(env, jobj, ageID, 23); //3.2
/Branch1:------for static method, No need create object-------/
/* 4. call method */
// 4.1 get method id (GetMethodID, GetStaticMethodID)
// 4.2 Preparation parameter
// 4.3 call method (CallVoidMethod, CallStaticVoidMethod)
mid = (*env)->GetStaticMethodID(env, cls, “main”,"([Ljava/lang/String;)V"); //4.1
if (mid == NULL) {
ret = -1;
printf(“can not get method\n”);
goto destroy;
}
(*env)->CallStaticVoidMethod(env, cls, mid, NULL); //4.3
/Branch2:------for no static method, Need create object-------/
/* 4. create object */
// 4.1 get constructor method id (GetMethodID)
// 4.2 create new object (NewObject)
cid = (*env)->GetMethodID(env, cls, “”, “()V”); //4.1
if (cid == NULL) {
ret = -1;
printf(“can not get constructor method\n”);
goto destroy;
}
jobj = (*env)->NewObject(env, cls, cid); //4.2
if (jobj == NULL) {
ret = -1;
printf("can not create object\n");
goto destroy;
}
/* 5. call method */
// 5.1 get method id (GetMethodID, GetStaticMethodID)
// 5.2 Preparation parameter
// 5.2 call method (CallVoidMethod, CallStaticVoidMethod)
mid = (*env)->GetMethodID(env, cls, "typeData","(Ljava/lang/String;)I"); //5.1
if (mid == NULL) {
ret = -1;
printf("can not get method\n");
goto destroy;
}
jstr = (*env)->NewStringUTF(env, "www.hceng.cn"); //5.2
r = (*env)->CallIntMethod(env, jobj, mid, jstr); //5.3
printf("%d\n", r);
destroy:
(*jvm)->DestroyJavaVM(jvm);
return ret;
}
{% endcodeblock %}
- 编译、设置环境变量:
javac Hello.java
javap -p -s Hello.class // get Signature
gcc -I /usr/lib/jvm/java-1.8.0-openjdk-amd64/include/ -I /usr/lib/jvm/java-1.8.0-openjdk-amd64/include/linux -o caller caller.c -L /usr/lib/jvm/java-8-openjdk-amd64/jre/lib/amd64/server/ -ljvm
LD_LIBRARY_PATH=/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/amd64/server/
./caller
- 结果:
java: name: hceng, age: 23
java: get str: www.hceng.cn ,will return 100
100
对该示例进行分析:
1.1 Java文件里有个
Hello
类,里面有两个静态变量、一个静态方法、一个非静态方法;
1.2 非静态方法typeData
需要传入一个参数和会返回一个整型;
2.1 将创建Java虚拟机封装成函数create_vm()
,以后有需要直接调用;
2.2 调用FindClass()
找到Java中要调用的类;
3.1 假如要修改属性,需要先得到属性的ID,针对静态和非静态属性,分别调用GetStaticFieldID()
、GetFieldID()
;
3.2 通过NewStringUTF()
得到可以在Java中使用的字符串;
3.3 通过SetStaticObjectField()
或SetObjectField()
设置Java中的字符串;
3.4 通过SetStaticIntField
或SetIntField()
设置Java中的整型;
4.1 如果要调用静态方法,就需要实例化对象;
4.2 通过GetStaticMethodID()
获得静态方法的ID;
4.3 通过CallStaticVoidMethod()
调用静态无返回值方法,最后一个参数是需要传入的参数;
5.1 如果要调用非静态方法,就需要先实例化对象;
5.2 通过GetMethodID()
获得构造方法的ID,对于构造方法,参数名字始终为<init>
;
5.3 通过NewObject()
实例化对象;
5.4 通过GetMethodID()
获得非静态方法ID;
5.5 通过CallIntMethod()
调用非静态方法,返回值就是Java方法的返回值;
6.销毁创建的Java虚拟机;
可以发现,要操作/调用Java中的属性或方法,都需要通过函数得到其ID,获取的函数又分为静态和非静态,然后再设置/调用等;
另外,获取ID的函数需要传入Signature
(JNI字段描述符),可通过javap -p -s Hello.class
命令,可以打印出对应类的Signature
;
4. Java的高级应用
4.1 泛型
泛型(Generics)是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
通俗的讲,泛型就是操作类型的占位符,即:假设占位符为T,那么此次声明的数据结构操作的数据类型为T类型。
比如下面的例子,在实例化对象时,可以指定不同数据类型,既可以是整型,也可以是字符串类型,从而打印的结果就完全不同。
-
示例:
{% codeblock lang:java [Generics.java] %}
//普通类
class Person1 {
private int age;public void setAge(int age) {
this.age = age;
}public int getAge() {
return this.age;
}
}
//泛型
class Person2 {
private T age;
public void setAge(T age) {
this.age = age;
}
public T getAge() {
return this.age;
}
}
//泛型接口
interface Person3 {
public void setAge(T age);
public T getAge();
}
//一般继承
class Student1 extends Person2 {
}
class Student2 extends Person2 {
}
//接口继承
class Student3 implements Person3 {
T age;
public void setAge(T age) {
this.age = age;
}
public T getAge() {
return this.age;
}
}
class Student4 implements Person3 {
String age;
public void setAge(String age) {
this.age = age;
}
public String getAge() {
return this.age;
}
}
public class Generics {
public static void main(String args[]) {
//常规方法,传入参数数据类型固定
Person1 p1 = new Person1();
p1.setAge(23);
System.out.println(p1.getAge());
System.out.println("--------------------------");
//泛型,传入参数数据类型可以不固定
Person2<Integer> p2 = new Person2<Integer>();
p2.setAge(24);
printInfo(p2);
genericsMethod(p2);
Person2<String> p3 = new Person2<String>();
p3.setAge("24 years old");
printInfo(p3);
genericsMethod(p3);
System.out.println("--------------------------");
//定义数据类型通用的对象
Person2<?> p4;
p4 = p2;
printInfo(p4); //无法 p4.setAge();
p4 = p3;
printInfo(p4);
System.out.println("--------------------------");
//子类继承:子类也泛型
Student1<Integer> s1 = new Student1<Integer>();
s1.setAge(10);
printInfo(s1);
genericsMethod(s1);
//子类继承:子类不泛型
Student2 s2 = new Student2();
s2.setAge("10 years old");
printInfo(s2);
genericsMethod(s2);
System.out.println("--------------------------");
//接口子类继承:子类也泛型
Student3<Integer> s3 = new Student3<Integer>();
s3.setAge(10);
System.out.println("interface:" + s3.getAge());
//子类继承:子类不泛型
Student4 s4 = new Student4();
s4.setAge("10 years old");
System.out.println("interface:" + s4.getAge());
System.out.println("--------------------------");
//受限泛型
printInfo1(s1); //参数限制了只能是Number类或其子类
//printInfo1(s2);
//printInfo2(s1);
printInfo2(s2); //参数限制了只能是String类或其父类
}
//方法参数通用
public static void printInfo(Person2<?> p) {
System.out.println("printInfo:" + p.getAge());
}
//方法的参数泛型
public static <T> void genericsMethod(Person2<T> p) {
System.out.println("genericsMethod:" + p.getAge());
}
//受限泛型
//上限:参数只能是Number类或其子类
public static void printInfo1(Person2<? extends Number> p) {
System.out.println("extends:" + p.getAge());
}
//下限:参数只能是String类或其父类
public static void printInfo2(Person2<? super String> p) {
System.out.println("extends:" + p.getAge());
}
}
{% endcodeblock %}
- 结果:
23
--------------------------
printInfo:24
genericsMethod:24
printInfo:24 years old
genericsMethod:24 years old
--------------------------
printInfo:24
printInfo:24 years old
--------------------------
printInfo:10
genericsMethod:10
printInfo:10 years old
genericsMethod:10 years old
--------------------------
interface:10
interface:10 years old
--------------------------
extends:10
extends:10 years old
对该示例进行分析:
1.1 首先是一个普通类
Person1
,包含一个私有int
类型数据,两个方法;
1.2 然后是一个泛型类Person2
,包含的元素同上,把int
换成了T
;
1.3 之后是一个泛型接口Person3
,也是使用T
代替数据类型,里面有两个方法模板;
1.4 接下来是两个类Student1
和Student2
继承于Person2
,前者数据类型是T
,后者是String
;
1.5 最后是两个类Student3
和Student4
继承于接口Person3
,前者数据类型是T
,后者是String
;
1.6 从这几个例子中,可以看到泛型可以用在类、接口和方法的创建中,称之为泛型类、泛型接口、泛型方法;
2.1main
里,首先是普通的类的实例化,传入int
类型数据,再打印;
2.2 接着是泛型的实例化,实例化时可以指定类里面的数据类型,这里的例子前者是int
类型数据,后者是String
类型数据;
2.3 接下来实例化对象p4
,使用通配符?
来代替数据类型,可以被其它数据类型赋值,但只能读取,不能设置;
2.4 然后是子类继承的实例化,前者定义类的时候类型是T
,仍有泛型的特性,实例化的时候指定为int
类型,后者定义类的时候类型是String
,为普通的类型,只能为字符串;
2.5 接口的继承也是,前面定义的时候,可以选择子类继续保持类泛型,或者变成普通类;
2.6 受限泛型,方法printInfo1
参数里有? extends Number
,表示上限,参数限制了只能是Number类或其子类;方法printInfo2
参数里有? super String
,表示下限,参数限制了只能是String类或其父类;
2.7 从这几个例子中,可以看到子类从父类继承,或者从接口继承,仍可以保持泛型的特性,也可以设置为普通类;此外还有受限泛型,限制泛型支持的数据类型范围;
4.2 反射
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;
这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
反射就是把java类中的各种成分映射成一个个的Java对象,例如:一个类有:成员变量、方法、构造方法、包等等信息,利用反射技术可以对一个类进行解剖,把个个组成部分映射成一个个对象。
- 示例:
{% codeblock lang:java [Reflect.java] %}
package hceng;
import java.lang.reflect.Method;
import java.lang.reflect.Field;
import java.lang.reflect.Constructor;
class Person {
private String name;
void setName(String name) {
this.name = name;
}
String getName() {
return this.name;
}
};
public class Reflect {
public static void main(String args[]) throws Exception {
//获得class的方法一
Class<?> c1 = Class.forName("hceng.Person"); //must be caught or declared to be thrown
//获得class的方法二
Person p = new Person();
Class<?> c2 = p.getClass();
//获得class的方法三
Class<?> c3 = Person.class;
System.out.println(c1.getName());
System.out.println(c2.getName());
System.out.println(c3.getName());
System.out.println("--------------------------");
int arr1[] = {1, 2, 3};
int arr2[][] = { {1, 2, 3}, {1,2} };
Class<?> a1 = arr1.getClass();
Class<?> a2 = arr2.getClass();
Class<?> a3 = int.class;
System.out.println(a1.getName());
System.out.println(a2.getName());
System.out.println(a3.getName());
System.out.println("--------------------------");
//类的实例化
Class<?> j = Class.forName("jack.Person");
Object p1 = j.newInstance(); //无参构造方法
Constructor<?> con = j.getConstructor(String.class); //有参构造方法
Object p2 = con.newInstance("hceng");
System.out.println("--------------------------");
//通过反射调用方法
Method set = j.getMethod("setName", String.class);
set.invoke(p1, "jack1");
set.invoke(p2, "jack2");
Method get = j.getMethod("getName");
System.out.println(get.invoke(p1));
System.out.println(get.invoke(p2));
System.out.println("--------------------------");
//通过修改属性
Field name = j.getDeclaredField("name"); //公共、私有的都可以访问
name.setAccessible(true); //因为name是私有的,需要先修改访问权限
name.set(p1, "hello, hceng");
name.set(p2, "hello, jack");
System.out.println(name.get(p1));
System.out.println(name.get(p2));
System.out.println("--------------------------");
//通过传入参数,灵活调用不同的包
Class<?> h = Class.forName(args[0]);
Constructor<?> c = h.getConstructor(String.class);
Object o = c.newInstance("hceng");
}
}
{% endcodeblock %}
{% codeblock lang:java [Person.java] %}
package jack;
public class Person {
private String name;
public void setName(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
//构造方法
public Person() {
System.out.println("Constructor1 of Person");
}
public Person(String name) {
this.name = name;
System.out.println("Constructor2 of Person, name is "+this.name);
}
};
{% endcodeblock %}
{% codeblock lang:java [Student.java] %}
package jack;
public class Student {
public String name;
public void setName(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
//构造方法
public Student() {
System.out.println("Constructor1 of Student");
}
public Student(String name) {
this.name = name;
System.out.println("Constructor2 of Student, name is "+this.name);
}
};
{% endcodeblock %}
编译:
javac -d . *.java
- 结果:
hceng@android:/work/java_learn/14th_reflect$ java hceng.Reflect jack.Person
hceng.Person
hceng.Person
hceng.Person
--------------------------
[I
[[I
int
--------------------------
Constructor1 of Person
Constructor2 of Person, name is hceng
--------------------------
jack1
jack2
--------------------------
hello, hceng
hello, jack
--------------------------
Constructor2 of Person, name is hceng
hceng@android:/work/java_learn/14th_reflect$ java hceng.Reflect jack.Student
hceng.Person
hceng.Person
hceng.Person
--------------------------
[I
[[I
int
--------------------------
Constructor1 of Person
Constructor2 of Person, name is hceng
--------------------------
jack1
jack2
--------------------------
hello, hceng
hello, jack
--------------------------
Constructor2 of Student, name is hceng
对该示例进行分析:
1.1 整个示例有三个文件
Reflect.java
、Person.java
、Student.java
,其中Reflect.java
在包hceng
里,其余两个在包jack
里;
1.2 在Person.java
里,定义了一个类Person
,包含一个私有变量、两个公共方法、两个构造方法(一个含参,一个不含);
1.3 在Student.java
里,定义了一个类Student
,包含一个私有变量、两个公共方法、两个构造方法(一个含参,一个不含);
2.1 在main
里,示例了三种获取类的方法:通过包名字、通过实例化的对象、直接通过类名字,注意每种方法都会产生异常,这里为了简洁,直接将异常抛出;
2.2 得到了类,可以通过getName
方法得到类完整名字;
3.1 定义一个一维数组,一个二维数组,一个整型数据类,得到其类名;
3.2 可以看到每种数据类型的类名都不一样;
4.1 通过包名,得到包jack.Person
里类;
4.2 对于无参的构造方法,直接调用newInstance
进行实例化;
4.3 有参的构造方法,先getConstructor
得到构造函数,再调用newInstance
传入参数实例化;
5.1 先通过getMethod
,传入参数为方法名和数据类型,得到类里面的方法;
5.2 再通过invoke
调用该方法,传入参数为实例化的对象和调用方法的参数;
6.1 通过getDeclaredField
得到类属性;
6.2 如果属性是私有属性,需要使用setAccessible
修改访问权限;
6.3 通过set
修改属性内容,通过get
获取属性内容;
7.1 之前通过import
导入需要的包,从而调用不同的类,在程序中固定了,现在可以将包名作为参数传入,实现灵活的调用;,
在Java运行中,可以通过三种方式得到运行期间的类:通过包名、通过实例化的对象、通过类名字;
通过这个类再实例化对象,修改属性或者调用方法等操作;
5. 其它
所有示例源码:
Github
参考资料:
韦东山第四期Android驱动_Java快速入门