Broadcast Receiver on Android

前言

Broadcast Receiver是Android组件中最基本也是最为常见用的四大组件之一。

Broadcast Receiver,广播接收者,顾名思义,是用来接收来自系统和应用中的广播。这种组件本质上是一种全局的监听器,用于监听系统全局的广播消息。

目录

  1. Broadcast Receiver的生命周期
  2. Broadcast的类型
  3. Broadcast Receiver的注册方式
  4. Broadcast Receiver的使用步骤

一、Broadcast Receiver的生命周期

BroadcastReceiver 有一个回调方法:void onReceive(Context curContext, Intent broadcastMsg)。当一个广播消息到达接收者时,Android 调用它的 onReceive() 方法并传递给它包含消息的 Intent 对象。BroadcastReceiver 被认为仅当它执行这个方法时是活跃的。当 onReceive() 返回后,它是不活跃的。

Android系统的很多消息(如系统启动,新短信,来电话等)都通过BroadcastReceiver来分发。BroadcastReceiver的生命周期是短暂的,而且是消息一到达则创建执行完毕就立刻销毁的。

拥有一个活跃状态的广播接收器的进程被保护起来而不会被杀死,但仅拥有失活状态组件的进程则会在其它进程需要它所占有的内存的时候随时被杀掉。 所以,如果响应一个广播信息需要很长的一段时 间,我们一般会将其纳入一个衍生的线程中去完成,而不是在主线程内完成它,从而保证用户交互过程的流畅。

这带来一个问题,当一个广播消息的响应时费时的,因此应该在独立的线程中做这些事,远离用户界面或者其它组件运行的主线程。如果 onReceive() 衍生线程然后返回,整个进程,包括新的线程,被判定为不活跃的(除非进程中的其它应用程序组件是活跃的),否则将使它处于被杀的危机。解决这个问题的方法是 onReceive() 启动一个 Service,让 Service 做这个工作,因此系统知道进程中有活跃的工作在做。

通常某个应用或系统本身在某些事件(电池电量不足、来电来短信)来临时会广播一个 Intent 出去,通过注册一个 BroadcastReceiver 来监听到这些 Intent,并获取其中广播的数据。

二、Broadcast的类型

Broadcast的类型有两种:普通广播和有序广播。

Normal broadcasts(普通广播):Normal broadcasts是完全异步的可以同一时间被所有的接收者接收到。通常每个接收者都无需等待即可以接收到广播,接收者相互之间不会有影响。消息的传递效率比较高。但缺点是接收者不能讲接收的消息的处理信息传递给下一个接收者也不能停止消息的传播。

Ordered broadcasts(有序广播):Ordered broadcasts的接收者按照一定的优先级进行消息的接收。如:A,B,C的优先级依次降低,那么消息先传递给A,在传递给B,最后传递给C。优先级别声明在中,取值为[-1000,1000]数值越大优先级别越高。优先级也可通过filter.setPriority(10)方式设置。 另外Ordered broadcasts的接收者可以通过abortBroadcast()的方式取消广播的传播,也可以通过setResultData和setResultExtras方法将处理的结果存入到Broadcast中,传递给下一个接收者。然后,下一个接收者通过getResultData()和getResultExtras(true)接收高优先级的接收者存入的数据。

三、Broadcast Receiver的注册方式

注册有两种方式:

1、静态注册:这种方法是在配置AndroidManifest.xml配置文件中的application里面定义receiver并设置要接收的action。通过这种方式注册的广播为常驻型广播,也就是说如果应用程序关闭了,有相应事件触发,程序还是会被系统自动调用运行。如:

1
2
3
4
5
6
7
<!-- 在配置文件中注册BroadcastReceiver能够匹配的Intent -->
<receiver android:name="com.example.test.MyBroadcastReceiver">
<intent-filter>
</action>
<category android:name="android.intent.category.DEFAULT"></category>
</intent-filter>
</receiver>

2、动态注册:这种方法是通过代码在.Java文件中进行注册。通过这种方式注册的广播为非常驻型广播,即它会跟随Activity的生命周期,所以在Activity结束前我们需要调用unregisterReceiver(receiver)方法移除它,否则会报异常。

1
2
3
4
5
6
//通过代码的方式动态注册MyBroadcastReceiver
MyBroadcastReceiver receiver=new MyBroadcastReceiver();
IntentFilter filter=new IntentFilter();
filter.addAction("android.intent.action.MyBroadcastReceiver");
//注册receiver
registerReceiver(receiver, filter);

要接收某些action,需要在AndroidManifest.xml里面添加相应的permission。

四、Broadcast Receiver的使用步骤

1、创建BroadcastReceiver的子类

由于BroadcastReceiver本质上是一种监听器,所以创建BroadcastReceiver的方法也非常简单,只需要创建一个BroadcastReceiver的子类然后重写onReceive (Context context, Intentintent)方法即可。

2、注册BroadcastReceiver

一旦实现了BroadcastReceiver,接下就应该指定该BroadcastReceiver能匹配的Intent即注册BroadcastReceiver。

另外,这是发送广播的代码:

1
2
3
​Intent intent = new Intent(String action);
intent.setAction(String action);
sendBroadcast(Intent);

Content Provider on Android

前言

Content Provider是Android组件中最基本也是最为常见用的四大组件之一。

主要用于对外共享数据,也就是通过Content Provider把应用中的数据共享给其他应用访问,其他应用可以通过Content Provider对指定应用中的数据进行操作。Content Provider分为系统的和自定义的,系统的也就是例如联系人,图片等数据。

Android中对数据操作包含有:

file, sqlite3, SharedPreferences, ContectResolver与ContentProvider。前三种数据操作方式都只是针对本应用内数据,程序不能通过这三种方法去操作别的应用内的数据。Android中提供ContectResolver与ContentProvider来操作别的应用程序的数据。

使用方式:

一个应用实现ContentProvider来提供内容给别的应用来操作

一个应用通过ContentResolver来操作别的应用数据,当然在自己的应用中也可以。

以下这段是Google Doc中对ContentProvider的大致概述:

内容提供者将一些特定的应用程序数据供给其它应用程序使用。内容提供者继承于Content Provider基类,为其它应用程序取用和存储它管理的数据实现了一套标准方法。然而,应用程序并不直接调用这些方法,而是使用一个 ContentResolver对象,调用它的方法作为替代。ContentResolver可以与任意内容提供者进行会话,与其合作来对所有相关交互通讯进行管理。

目录

  1. 为什么使用Content Provider?
  2. Content Provider的关键类
  3. ContentProvider的使用
  4. 监听ContentProvider中数据的变化

一、为什么使用Content Provider?

关于数据共享,以前我们学习过文件操作模式,知道通过指定文件的操作模式为Context.MODE_WORLD_READABLE或Context.MODE_WORLD_WRITEABLE同样也可以对外共享数据。那么,这里为何要使用ContentProvider对外共享数据呢?

是这样的,如果采用文件操作模式对外共享数据,数据的访问方式会因数据存储的方式而不同,导致数据的访问方式无法统一,如:采用xml文件对外共享数据,需要进行xml解析才能读取数据;采用sharedpreferences共享数据,需要使用sharedpreferences API读取数据。

使用ContentProvider对外共享数据的好处是统一了数据的访问方式。

二、Content Provider的关键类

1、ContentProvider

Android提供了一些主要数据类型的ContentProvider,比如音频、视频、图片和私人通讯录等。可在android.provider包下面找到一些Android提供的ContentProvider。通过获得这些ContentProvider可以查询它们包含的数据,当然前提是已获得适当的读取权限。

主要方法:

1
2
3
4
5
6
public boolean onCreate() 在创建ContentProvider时调用;
public Cursor query(Uri, String[], String, String[], String) 用于查询指定Uri的ContentProvider,返回一个Cursor;
public Uri insert(Uri, ContentValues) 用于添加数据到指定Uri的ContentProvider中;
public int update(Uri, ContentValues, String, String[]) 用于更新指定Uri的ContentProvider中的数据;
public int delete(Uri, String, String[]) 用于从指定Uri的ContentProvider中删除数据;
public String getType(Uri) 用于返回指定的Uri中的数据的MIME类型。

public String getType(Uri) 用于返回指定的Uri中的数据的MIME类型

1)如果操作的数据属于集合类型,那么MIME类型字符串应该以vnd.android.cursor.dir/开头。

例如:要得到所有person记录的Uri为content://contacts/person,那么返回的MIME类型字符串为”vnd.android.cursor.dir/person”。

2)如果要操作的数据属于非集合类型数据,那么MIME类型字符串应该以vnd.android.cursor.item/开头。

例如:要得到id为10的person记录的Uri为content://contacts/person/10,那么返回的MIME类型字符串应为”vnd.android.cursor.item/person”。

2、ContentResolver

当外部应用需要对ContentProvider中的数据进行添加、删除、修改和查询操作时,可以使用ContentResolver类来完成,要获取ContentResolver对象,可以使用Context提供的getContentResolver()方法。

ContentResolver cr = getContentResolver();

ContentResolver提供的方法和ContentProvider提供的方法对应的有以下几个方法。

1
2
3
4
public Uri insert(Uri uri, ContentValues values) 用于添加数据到指定Uri的ContentProvider中;
public int delete(Uri uri, String selection, String[] selectionArgs) 用于从指定Uri的ContentProvider中删除数据;
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) 用于更新指定Uri的ContentProvider中的数据;
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) 用于查询指定Uri的ContentProvider。

3、Uri解析类

Uri指定了将要操作的ContentProvider,其实可以把一个Uri看作是一个网址,我们把Uri分为三部分:

第一部分是”content://“,可以看作是网址中的”http://“;

第二部分是主机名或authority,用于唯一标识这个ContentProvider,外部应用需要根据这个标识来找到它。可以看作是网址中的主机名,如”blog.csdn.net”;

第三部分是路径名,用来表示将要操作的数据。可以看作网址中细分的内容路径。

Uri.png

因为Uri代表了要操作的数据,所以我们经常需要解析Uri,并从Uri中获取数据。Android系统提供了两个用于操作Uri的工具类,分别为UriMatcher和ContentUris 。

1)UriMatcher类用于匹配Uri。

1、把你需要匹配Uri路径全部给注册上

1
2
3
4
5
6
7
8
9
10
11
12
//常量UriMatcher.NO_MATCH表示不匹配任何路径的返回码
UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
sMatcher.addURI("com.cyw.provider.personprovider", "person", 1);
sMatcher.addURI("com.cyw.provider.personprovider", "person/#", 2);//#号为通配符
switch (sMatcher.match(Uri.parse("content://com.cyw.provider.personprovider/person/10"))) {
case 1
break;
case 2
break;
default://不匹配
break;
}

2、使用sMatcher.match(uri)方法对输入的Uri进行匹配,如果匹配就返回匹配码,匹配码是调用addURI()方法传入的第三个参数​

2)ContentUris类用于操作Uri路径后面的ID部分,它有两个比较实用的方法:

1、withAppendedId(uri, id)用于为路径加上ID部分:

1
2
3
Uri uri = Uri.parse("content://com.cyw.provider.personprovider/person")
Uri resultUri = ContentUris.withAppendedId(uri, 10);
//生成后的Uri为:content://com.cyw.provider.personprovider/person/10

2、parseId(uri)方法用于从路径中获取ID部分:

1
2
Uri uri = Uri.parse("content://com.cyw.provider.personprovider/person/10")
long personid = ContentUris.parseId(uri);//获取的结果为:10

三、ContentProvider的使用

1、需要继承ContentProvider并重写方法

2、需要在AndroidManifest.xml使用对该ContentProvider进行配置,为了能让其他应用找到该ContentProvider ,ContentProvider采用了authorities(主机名/域名)对它进行唯一标识,你可以把ContentProvider看作是一个网站,authorities 就是域名。

四、监听ContentProvider中数据的变化

1、如果ContentProvider的访问者需要知道ContentProvider中的数据发生变化,可以在ContentProvider发生数据变化时调用getContentResolver().notifyChange(uri, null)来通知注册在此URI上的访问者,例子如下:

1
2
3
4
5
6
public class PersonContentProvider extends ContentProvider {
public Uri insert(Uri uri, ContentValues values) {
db.insert("person", "personid", values);
getContext().getContentResolver().notifyChange(uri, null);
}
}

2、如果ContentProvider的访问者需要得到数据变化通知,必须使用ContentObserver对数据(数据采用uri描述)进行监听,当监听到数据变化通知时,系统就会调用ContentObserver的onChange()方法:

1
2
3
4
5
6
7
8
9
10
getContentResolver().registerContentObserver(Uri.parse("content://com.cyw.providers.personprovider/person"),
true, new PersonObserver(new Handler()));
public class PersonObserver extends ContentObserver{
public PersonObserver(Handler handler) {
super(handler);
}
public void onChange(boolean selfChange) {
//此处可以进行相应的业务处理
}
}

Introduction to Leap Motion

前言

最近是要完成一个关于Leap Motion的项目,所以就顺便到官网翻译了下文章。一下首先是翻译的可以直接上手的Setting up a project,以及Hello World

目录

1.Setting up a project

2.Hello World

1.Setting up a project

Leap Motion java SDK 使用的封装了Leap Motion API 类定义的标准jar包,以及一系列的原生库(native libraries),让你可以开发Leap的java程序,与Leap Motion进行数据交换。通常,创建一个java项目包含以下步骤:添加LeapJava.jar文件到你的项目的classpath文件中,设置JVM库路径参数以至于你的JVM能够找得到Leap Motion的原生库。

Leap Motion Java libraries

Leap Motion的jar包是跨平台的,但是他的原生库必须要与系统平台以及用于执行程序的JVM的体系结构进行匹配。

为了能偶在Java程序中使用到Leap Motion的SDk,你需要添加LeapJava.jar文件到classpath文件中,然后设置java库的路径到Leap Motion原生库的位置。

需要使用到Leap Motion Java SDK的以下Java和原生库。

  • 32-bit Windows:
    • LeapSDK/lib/LeapJava.jar–Leap Motion Java API的类定义。
    • LeapSDk/lib/x86/LeapJava.dll–在Windows系统下的32-bit Leap Motion Java库。
    • LeapSDK/lib/x86/Leap.dll–在Windows系统下的32-bit Leap Motion库。
  • 64-bit Windows:
    • LeapSDK/lib/LeapJava.jar–Leap Motion Java API的类定义。
    • LeapSDK/lib/x64/LeapJava.dll–在Windows系统下的64-bit Leap Motion Java库。
    • LeapSDK/lib/x64/Leap.dll–在Windows系统下的64-bit Leap Motion库。
  • 32- or 64-bit Mac OS:
    • LeapSDK/lib/LeapJava.jar–Leap Motion Java API的类定义。
    • LeapSDK/lib/libLeapJava.sylib–Mac系统下的Leap Motion Java库。
    • LeapSDK/lib/libLeap.sylib–Mac系统下的Leap Motion库。

Compile from the command line

使用java编译器,javac编译,设置classpath选项以指定LeapJar文件。举个例子,要编译包含在Leap Motion SDk中的Sample.java,你可以使用一下命令行:

1
2
3
4

javac -classpath <LeapMotion>/lib/LeapJava.jar Sample.java


(是指你安装的Leap Motion SDK所在的文件夹)

Run from the command line

为了运行Leap驱动的程序,java在运行时需要找到Leap Motion原生本地库。LeapJava.jar必须已经设置在classpath文件里面了。你可以设置java的’java.library.path’参数以识别原生库。更关键的是,在Windows系统下,你必须制定32-bit 或者64-bit库以匹配你使用的JVM的体系架构。

在MAC上,你可以使用一下命令行运行’Sample’程序:

1
2
3

java -classpath ".:<LeapSDK>/lib/LeapJava.jar" -Sjava.library.path=<LeapSDK>/lib Sample

在Windows下,你可以使用以下命令行在64-bit JVm下运行Sample程序:

1
2
3
4

java -classpath ".;<LeapSDK>/lib/LeapJava.jar" -Djava.library.path=<LeapSDK>/lib/x64 Sample


Eclipse

在eclipse IDE里,你要添加LeapJava.jar文件到项目工程中,作为一个外部jar包,然后设置正确的原生Leap Motion库的路径为Jar包的一个属性。

  1. 在Eclipse的文件菜单上选择New > Java Project.

  2. Create Java Project页面上给项目工程指定一个名称,然后如必要的话设置其他属性(Leap Motion SDK支持Java 6和7)

  3. 点击Next键,进入Java Setting页面

  4. 选择Livbrary选项

  5. 点击*Add External Jars…*按钮

  6. 导航到LeapJava.jar包

  7. 点击Open键,添加LeapJava.jar到项目工程中

  8. 接着,在Library列表里面点击LeapJava.jar条目前面的小三角形,出现Library的属性
    java setting page

  9. 选择Native Library location条目

  10. 点击Edit按钮

  11. 导航到半酣Leap Motion原生库的文件夹

在Windows下,需要确保你要选择的文件夹下包含适合你的目标体系架构的正确的库。如果你的是32-bit JVM,要用SDk里面x86文件夹下的Leap Motion库。如果你的是64-bit JVM,要用SDk里面x64文件夹下的Leap Motion库。在Mac上,Leap Motion库支持任意体系架构。

  1. 点击OK设置路径

注意:或者你也可以通过项目属性对话框添加Leap Motion库到一个已经存在的项目工程之中。

IntelliJ

在IntelliJ IDE上,你要添加LeapJava.jar文件到项目工程中作为一个库,需要分别使用JVM参数设置Leap Motion原生库的路径,’java.library.path’.JVM 参数可以通过使用IntelliJ Run/Debug组态来设置。

以下步骤添加LeapJava.jar到项目工程中:

  1. 按通常的方法创建了一个工程之后,选择File > Project Structure菜单打开设置对话框
  2. 在project Setting下点击Library
  3. 点击在库列表顶部的小’+’号按钮,打开Select Library Files对话框
  4. 从你的Leap Motion SDK添加’LeapJava.jar’

通过创建一个Run/Debug configuration来设置到原生Leap Motion库的路径:

  1. 选择Run > Edit Configurations… 菜单
  2. 点击在配置离别上面的小’+’号按钮
  3. 选择应用,创建一个新的应用配置
  4. 指定一个名称
  5. 设置VM选项域来设置’-Djava.library.path’参数指向Leap Motion原生库所在的文件夹
  6. 点击OK

IntelliJ

在Windows下,确保选择跟你的JVM体系匹配的文件夹。如果你的是32-bit JVM,要用SDk里面x86文件夹下的Leap Motion库。如果你的是64-bit JVM,要用SDk里面x64文件夹下的Leap Motion库。在Mac上,Leap Motion库支持任意体系架构。

NetBeans

在NetBeans IDE下,你要添加LeapJava.jar文件到项目工程中作为一个库,需要分别使用JVM参数设置Leap Motion原生库的路径,’java.library.path’.JVM 参数可以通过使用IntelliJ Run/Debug组态来设置。

以下步骤添加LeapJava.jar到项目工程中:

  1. 按通常的方法创建了一个工程之后,选择File > Project Properties菜单打开设置对话框
  2. 点击Library
  3. 点击Add Jar/Folder 按钮
  4. 在你的Leap Motion SDk中找到’LeapJava.jar’
  5. 点击OK添加jar包到你的project中
  6. 接着,在你的项目属性列表中点击Run
  7. 设置VM选项域来设置’-Djava.library.path’参数指向Leap Motion原生库所在的文件夹

NetBeans

在Windows下,确保选择跟你的JVM体系匹配的文件夹。如果你的是32-bit JVM,要用SDk里面x86文件夹下的Leap Motion库。如果你的是64-bit JVM,要用SDk里面x64文件夹下的Leap Motion库。在Mac上,Leap Motion库支持任意体系架构。

2. Hello World

这篇文章演示了如何连接Leap Motion控制器,还有基本的访问跟踪数据操作。在阅读完这篇文章之后以及紧随文章编写属于你自己的基本程序,你应该拥有了扎实的基础,可以开始你的应用开发之旅了。

首先,是一小段的背景知识…

How the Leap Motion Controller Works

Leap Motion控制器着重的包括硬件和软件组件。

Leap Motion硬件主要包括一对立体声红外摄像机和照明LED的。摄像传感器朝上(当该装置处于其标准定向的时候)。下面的这个插图从Leap Motion传感器的视角来展示了一名用户的手。

hands

Leap Motion软件接收来自传感器的数据,然后分析这个数据,尤其是手掌、手指、手臂,或者工具的数据(追踪其他类型的数据,可能会在将来添加对应的对象的,而这是当前能跟踪的实体)。软件保持着人手的内部模型,然后与传感器发送来的数据组成的模型进行比较,以决定最好的匹配样式。传感器数据将会一帧一帧的被分析,设备会传送每一帧的数据到你的Leap Motion驱动的应用里面。被你的应用接收到的’Frame’对象包含所有已知的位置,速度和跟踪实体的辨识。比如手掌,手指,还有工具。构造,比如说手势,和跨越多个帧的动作,都需要不断刷新每一帧。如果需要控制器提供的跟踪数据的概述的话,可以阅读API Overview

Leap Motion软件可以在客户端计算机上作为一种服务(Windows)或者是一个守护线程(Mac和Linux)。原生的Leap Motion驱动的应用可以使用Leap Motion提供的动态库(作为Leap Motion SDk的一部分提供)连接到这个服务。web应用可以连接到有服务托管的WebSocket服务器上面。WebSocket提供以JSON格式信息的跟踪数据–每一帧数据每一条信息。JavaScript库,LeapJS,提供了包含这个数据的API。想要更多信息的话,可以阅读System Architecture

Set Up the Files

这个教程也使用命令行编译器和连接器(如有必要),为了让我们更加庄主与代码而不是环境。对于如何在主流IDE上使用Leap Motion SDK创建工程,可以先看看这篇文章Setting up a Project

  1. 如果你还没有准备好,那么可以从Developer site下载解压最新版本的Leap Motion SDk,然后安装最新的Leap Motion服务。
  2. 打开一个终端或者控制台窗口,导航到SDk ‘Sample’文件夹下。
  3. ‘Sample.java’包含了这篇教程的最终代码,但是出于能够更加深入理解本篇教程的目的下,你可以重命名已经存在的这个Sample文件,然后在这个文件夹下创建一个新的、空白的’Sample.java’文件。保留已经存在的Sample文件,以供参考。
  4. 在你的’Sample.java’文件中,添加导入Leap Motion库的代码:
1
2
3
4

import com.leapmotion.leap.*;


  1. 添加主题代码,定义一个java命令行程序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14

class Sample {
public static void main(String[] args) {

// Keep this process running until Enter is pressed
System.out.println("Press Enter to quit...");
try {
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
}

这个代码简单的打印了一条信息,然后等键盘输入任意键后退出。

Get Connected

下一步是在程序中添加一个控制器对象Controller–这个对象将会帮助我们连接到Leap Motion 服务/守护线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

class Sample {
public static void main(String[] args) {
Controller controller = new Controller();

// Keep this process running until Enter is pressed
System.out.println("Press Enter to quit...");
try {
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
}

当你创建一个Controller对象的时候,他会自动的链接到Leap Motion设备,一旦连接建立,你可以通过使用Controlller.frame()方法得到所要的跟踪数据。

这个连接的过程是异步的,所以你不能在一行创建’Controller’,然后期待能偶在下一行语句得到数据。你必须等到连接完成之后才行。但是这需要多久呢?

To Listen or not to Listen?

你可以在Controlller对象上添加监听器对象Listener,这回提供一个基于事件的响应机制以响应’Controlller’的状态变化。这是本篇教程使用的方法–但是他也不总是最好的办法。

监听器的问题:Listener对象使用了独立的线程来调用你实现的每一个事件的代码。所以,使用监听器机制可以展示出贯穿你的代码中的复杂性。你需要确保的是,在你的监听器子类中实现的代码能够以一种线程安全的方式访问到你的程序。比如,你可能无法访问到跟GUI控制器有关的变量,除了主线程以外。此外,还可能有与线程的创建和清理相关的额外开销。

**Avoiding Listeners:**你可以禁止使用监听器对象通过简单地轮询Controller对象的帧(或其他的状况),如果对于你的程序是必要的话。很多程序都已经有了一个事件循环,或者动画循环来驱动用户的输入和动画。如果这样的话,你可以在每一轮的循环中获得数据,这将尽可能的加快你使用数据的速度。

API中监听器类定义了当Controller事件触发时会被调用的函数的签名。你可以通过创建’listener’的子类,和实现你感兴趣的事件的回调函数的方式,创建一个监听器。

那么,继续我们的教程,添加’SampleListener’类到你的程序之中。为了简单,我们添加’SampleListener’类到’Sample’类一样的文件下面吧。

1
2
3
4
5
6
7
8
9
10
11
12

class SampleListener extends Listener {

public void onConnect(Controller controller) {
System.out.println("Connected");
}

public void onFrame(Controller controller) {
System.out.println("Frame available");
}
}

如果你已经看过了官方给的示例代码,你可能已经注意到了,回调函数的多次出现。你可能也想着吧他们都添加到自己的文件里面,如果你希望的话,但是为了保持代码简单,我们希望只专注于0nConnect()和onFrame()方法。

现在使用你刚写的类创建一个’SampleListener’对象。然后把它添加到你的控制器里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

class Sample {
public static void main(String[] args) {
// Create a sample listener and controller
SampleListener listener = new SampleListener();
Controller controller = new Controller();

// Have the sample listener receive events from the controller
controller.addListener(listener);

// Keep this process running until Enter is pressed
System.out.println("Press Enter to quit...");
try {
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}

// Remove the sample listener when done
controller.removeListener(listener);
}
}

现在,你恶意运行测试一下你的’Sample’程序了。可以跳到后面查看Running the Sample的内容。

如果代码什么的搜没有错误的话(还有就是你的Leap Motion硬件设备已经连接到计算机),那么你会看到这样的字符串”Connect”打印在终端窗口上,紧接着的是一系列以很快的速度输出的”Frame available”。如果报错而且你搞不懂为什么的话,你可以在我们的开发者论坛上向我们寻求帮助。

无论你什么时候开发出错,都可以试着打开可视化诊断工具。这个程序将展示一个Leap Motion跟踪数据的可视化观察器。你可以比较你在程序中看到的和你在可视化观察器中看到的(这两者都是用的相同的API),来查找问题。

On Connect

当你的控制器对象成功连接到Leap Motion服务/守护线程之后,而且你的Leap Motion硬件已经插入计算机,控制器对象将会改变它的isConnect()方法的属性为’true’,然后调用你的Listener.onConnect的回调。

控制器连接之后,你可以使用Controller.enableGesture()Controller.setPolicy()方法设置控制器的参数。比如:你可以通过以下’onConnect()’方法来设置滑动手势的使能化:

1
2
3
4
5
6

public void onConnect(Controller controller) {
System.out.println("Connected");
controller.enableGesture(Gesture.Type.TYPE_SWIPE);
}

On Frame

在Leap Motion系统中,所有跟踪数据送达都是通过Frame对象来实现的。你可以从你的控制器(当他连接之后)通过调用Controller.frame()方法来获得’Frame’对象。当一个新的frame数据有效时,你的Listener子类的onFrame()回调方法将会被调用。当你不在使用监听器时,你可以比较id()的值和你处理的上一帧,判断是否是一个新的frame(帧)。注意,通过设置frame()函数的’history’参数,你可以获得更早之前的帧(里面至少有60以上的帧的数据被保存着)。所以,如有必要的话,你可以使轮询速率变得比Leap Motion数据帧的速率还慢,可以处理每一帧的数据。

为了获得数据帧frame,要添加frame()方法到你的onframe()回调函数里面:

1
2
3
4
5

public void onFrame(Controller controller) {
Frame frame = controller.frame();
}

然后,打印出Frame对象的一些属性:

1
2
3
4
5
6
7
8
9
10
11
12

public void onFrame(Controller controller) {
Frame frame = controller.frame();

System.out.println("Frame id: " + frame.id()
+ ", timestamp: " + frame.timestamp()
+ ", hands: " + frame.hands().count()
+ ", fingers: " + frame.fingers().count()
+ ", tools: " + frame.tools().count()
+ ", gestures " + frame.gestures().count());
}

那么,再次运行你的Sample,把一只或者两支手掌放在Leap Motion设备上面,你会在控制台窗口看到每一帧的基本属性。

本次教程在这就结束了,但是你可以看看官方提供的Sample文件中的其他代码。比如说,在一个Frame里面如何获得手掌Hand,手指Finger,手臂Arm,骨头Bone 还有手势Gesture对象。

Running the Sample

运行一个示例应用:

  1. 编译示例应用:

    • 在Windows系统下,确保’Sample.java’和’LeapJava.jar’在当前文件夹下面,然后输入以下命令行在命令行提示符后面,并运行:

      1
      javac -classpath LeapJava.jar Sample.java
    • 在Mac系统上,确保’Sample.java’和’LeapJava.jar’在当前文件夹下面,然后在终端里面运行以下命令行:

    1
    2
    3

    javac -classpath ./LeapJava.jar Sample.java

  2. 运行示例应用:

    • 在Windows系统下,确保’Sample.class’,’LeapJava.jar’,’LeapJava.dll’,和’Leap.dll’在同一个文件夹下。如果你是用的是32-bit版本的JVM,需要使用SDk下的lib\x86文件夹下的.dll文件。如果你是用的是64-bit版本的JVM,需要使用SDk下的lib\x64文件夹下的.dll文件.在命令行提示符后输入并运行以下命令行:

      1
      java -classpath "LeapJava.jar;." Sample
    • 在Mac系统下,确保’Sample.class’,’LeapJava.jar’,’LeapJava.dll’,和’Leap.dll’在同一个文件夹下。在终端中运行命令行:

    1
    2
    3

    java -classpath "./LeapJava.jar:." Sample

当应用程序初始化并连接之后到Leap之后,你会看到”Connect”信息被打印在标准输出中。你还会看到frame的信息,在每一次’onframe’事件被调用后。

Java 关键字总结

前言

| 访问控制 | 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 | throw | throws | finally |
| 包相关 | import | package |
| 基本类型 | boolean | byte | char | double | float | int | long |
| | short | null | true | false |
| 变量引用 | super | this | void |
| 保留字 | goto | const |

详细解释

1、访问控制

  1. private 私有的

private 关键字是访问控制修饰符,可以应用于类、方法或字段(在类中声明的变量)。 只能在声明 private(内部)类、方法或字段的类中引用这些类、方法或字段。在类的外部或者对于子类而言,它们是不可见的。

  1. protected 受保护的

protected 关键字是可以应用于类、方法或字段(在类中声明的变量)的访问控制修饰符。可以在声明 protected 类、方法或字段的类、同一个包中的其他任何类以及任何子类(无论子类是在哪个包中声明的)中引用这些类、方法或字段。

  1. public 公共的

public 关键字是可以应用于类、方法或字段(在类中声明的变量)的访问控制修饰符。 可能只会在其他任何类或包中引用 public 类、方法或字段。

那么我们总结一下,Java之中的权限访问修饰符(其实还有一种权限访问情况,就是默认情况,暂且称作default吧):

| 访问权限 | 当前类 | 包 | 子类 | 其他包 |
| public | ∨ | ∨ | ∨ | ∨ |
| protect | ∨ | ∨ | ∨ | × |
| default | ∨ | ∨ | × | × |
| private | ∨ | × | × | × |

2、类、方法和变量修饰符

  1. abstract 声明抽象

abstract关键字可以修改类或方法。abstract类可以扩展(增加子类),但不能直接实例化。abstract方法不在声明它的类中实现,但必须在某个子类中重写。采用 abstract方法的类本来就是抽象类,并且必须声明为abstract。

  1. class类

class 关键字用来声明新的 Java 类,该类是相关变量和/或方法的集合。类是面向对象的程序设计方法的基本构造单位。类通常代表某种实际实体,如几何形状或人。类是对象的模板。每个对象都是类的一个实例。要使用类,通常使用 new 操作符将类的对象实例化,然后调用类的方法来访问类的功能。

  1. extends 继承、扩展

extends 关键字用在 class 或 interface 声明中,用于指示所声明的类或接口是其名称后跟有 extends 关键字的类或接口的子类。子类继承父类的所有 public 和 protected 变量和方法(但是不包括构造函数)。 子类可以重写父类的任何非 final 方法。一个类只能扩展一个其他类。

extends 关键字用在 class 或 interface 声明中,用于指示所声明的类或接口是其名称后跟有 extends 关键字的类或接口的子类。

  1. final 最终、不可改变

在Java中,final关键字可以用来修饰类、方法和变量(包括成员变量和局部变量)。final方法在编译阶段绑定,称为静态绑定(static binding)。下面就从这四个方面来了解一下final关键字的基本用法。

1、修饰类

当用final修饰一个类时,表明这个类不能被继承,不能有子类。也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。final类中的成员变量可以根据需要设为final,但是要注意final类中的所有成员方法都会被隐式地指定为final方法。

2、修饰方法

下面这段话摘自《Java编程思想》:

使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升。在最近的Java版本中,不需要使用final方法进行这些优化了。

因此,如果只有在想明确禁止 该方法在子类中被覆盖的情况下才将方法设置为final的。

还有就是,类的private方法会隐式地被指定为final方法。

3、修饰变量

修饰变量是final用得最多的地方。

对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。引用变量被final修饰之后,虽然不能再指向其他对象,但是它指向的对象的内容是可变的。

4、final参数

当函数参数为final类型时,你可以读取使用该参数,但是无法改变该参数的值或者引用指向。道理同final变量。

概括起来就是:

在A类是声明为final类型的方法,那么不能在子类里被覆盖;

如果A类被声明为final类型的类,那么B类不能继承A类;

如果成员变量声明为final类型,那么成员变量不能被修改;

注意:

1、一个类不能同时是 abstract 又是 final。abstract 意味着必须扩展类,final 意味着不能扩展类。一个方法不能同时是 abstract 又是 final。abstract 意味着必须重写方法,final 意味着不能重写方法。两者是相互矛盾的。

2、当用final作用于类的成员变量时,成员变量(注意是类的成员变量,局部变量只需要保证在使用之前被初始化赋值即可)必须在定义时或者构造器中进行初始化赋值,而且final变量一旦被初始化赋值之后,就不能再被赋值了。

3、final变量和普通变量的区别。当final变量是基本数据类型以及String类型时,如果在编译期间能知道它的确切值,则编译器会进行优化,会把它当做编译期常量使用。也就是说在用到该final变量的地方,相当于直接访问的这个常量,不需要在运行时确定。这种和C语言中的宏替换有点像。而普通变量在编译时,确定不了自身的值,需要在运行时才能知道。

4、局部内部类和匿名内部类只能访问局部final变量。因为这里的局部变量,需要在编译阶段便需要确定下来的。也就是说,如果局部变量的值在编译期间就可以确定,则直接在匿名内部里面创建一个拷贝。如果局部变量的值无法在编译期间确定,则通过构造器传参的方式来对拷贝进行初始化赋值。

  1. implements实现

implements 关键字在 class 声明中使用,以指示所声明的类提供了在 implements 关键字后面的名称所指定的接口中所声明的所有方法的实现。类必须提供在接口中所声明的所有方法的实现。一个类可以实现多个接口。

  1. interface 接口

interface 关键字用来声明新的 Java 接口,接口是方法的集合。

接口是 Java 语言的一项强大功能。任何类都可声明它实现一个或多个接口,这意味着它实现了在这些接口中所定义的所有方法。

实现了接口的任何类都必须提供在该接口中的所有方法的实现。一个类可以实现多个接口。

  1. native 本地

native 关键字可以应用于方法,以指示该方法是用Java以外的语言实现的,方法对应的实现不是在当前文件,而是在用其他语言(如C和C++)实现的文件中。。

Java不是完美的,Java的不足除了体现在运行速度上要比传统的C++慢许多之外,Java无法直接访问到操作系统底层(如系统硬件等),为此Java使用native方法来扩展Java程序的功能。

可以将native方法比作Java程序同C程序的接口,其实现步骤:

1、在Java中声明native()方法,然后编译;

2、用javah产生一个.h文件;

3、写一个.cpp文件实现native导出方法,其中需要包含第二步产生的.h文件(注意其中又包含了JDK带的jni.h文件);

4、将第三步的.cpp文件编译成动态链接库文件;

5、在Java中用System.loadLibrary()方法加载第四步产生的动态链接库文件,这个native()方法就可以在Java中被访问了。

JAVA本地方法适用的情况

1、为了使用底层的主机平台的某个特性,而这个特性不能通过JAVA API访问

2、为了访问一个老的系统或者使用一个已有的库,而这个系统或这个库不是用JAVA编写的

3、为了加快程序的性能,而将一段时间敏感的代码作为本地方法实现。

  1. new 新,创建

new 关键字用于创建类的新实例。

new 关键字后面的参数必须是类名,并且类名的后面必须是一组构造方法参数(必须带括号)。
参数集合必须与类的构造方法的签名匹配。

= 赋值号左侧的变量的类型必须与要实例化的类或接口具有赋值兼容关系。

  1. static 静态

static可以用于修饰属性,可以修饰代码块,也可以用于修饰方法,还可以用于修饰类。

1、static修饰属性:无论一个类生成了多少个对象,所有这些对象共同使用唯一一份静态的成员变量;一个对象对该静态成员变量进行了修改,其他对象的该静态成员变量的值也会随之发生变化。如果一个成员变量是static的,那么我们可以通过‘类名.成员变量名’的方式来使用它。

2、static修饰方法:static修饰的方法叫做静态方法。对于静态方法来说,可以使用‘类名.方法名’的方式来访问。静态方法只能继承,不能重写(Override),因为重写是用于表现多态的,重写只能适用于实例方法,而静态方法是可以不生成实例直接用类名来调用,这就会与重写的定义所冲突,与多态所冲突,所以静态方法不能重写,只能是隐藏。

static方法与非static方法:不能在静态方法中访问非静态成员变量;可以在静态方法中访问静态的成员变量。可以在非静态方法中访问静态的成员变量:因为静态方法可以直接用类名来调用,而非静态成员变量是在创建对象实例时才为变量分配内存和初始化变量值。

不能在静态方法中使用this关键字:因为静态方法可以直接用类名来调用,而this实际上是创建实例时,实例对应的一个应用,所以不能在静态方法上使用this。

3、static修饰代码块:静态代码块。静态代码块的作用也是完成一些初始化工作。首先执行静态代码块,然后执行构造方法。静态代码块在类被加载的时候执行,而构造方法是在生成对象的时候执行;要想调用某个类来生成对象,首先需要将类加载到Java虚拟机上(JVM),然后由JVM加载这个类来生成对象。

类的静态代码块只会执行一次,是在类被加载的时候执行的,因为每个类只会被加载一次,所以静态代码块也只会被执行一次;而构造方法则不然,每次生成一个对象的时候都会调用类的构造方法,所以new一次就会调用构造方法一次。如果继承体系中既有构造方法,又有静态代码块,那么首先执行最顶层的类的静态代码块,一直执行到最底层类的静态代码块,然后再去执行最顶层类的构造方法,一直执行到最底层类的构造方法。注意:静态代码块只会执行一次。

4、static修饰类:这个有点特殊,首先,static是可以用来修饰类的,但是static是不允许用来修饰普通类,只能用来修饰内部类,被static所修饰的内部类可以用new关键字来直接创建一个实例,不需要先创建外部类实例。static内部类可以被其他类实例化和引用(即使它是顶级类)。

其实理解起来也简单。因为static主要是修饰类里面的成员,包括内部类、属性、方法这些。修饰这些变量的目的也很单纯,那就是暗示这个成员在该类之中是唯一的一份拷贝,即便是不断的实例化该类,所有的这个类的对象都会共享这些static成员。这样就好办了。因为是共享的、唯一的,所以,也就不需要在实例化这个类以后再通过这个类来调用这个成员了,显然有点麻烦,所以就简单一点,直接通过类名直接调用static成员,更加直接。然而这样设置之后,就出现了一个限制,就是,static方法之中不能访问非static属性,因为这个时候非static属性可能还没有给他分配内存,该类还没有实例化。

所以,通常,static 关键字意味着应用它的实体在声明该实体的类的任何特定实例外部可用。

可以从类的外部调用 static 方法,而不用首先实例化该类。这样的引用始终包括类名作为方法调用的限定符。

  1. strictfp 严格,精准

strictfp的意思是FP-strict,也就是说精确浮点的意思。在Java虚拟机进行浮点运算时,如果没有指定strictfp关键字时,Java的编译器以及运行环境在对浮点运算的表达式是采取一种近似于我行我素的行为来完成这些操作,以致于得到的结果往往无法令人满意。而一旦使用了strictfp来声明一个类、接口或者方法时,那么所声明的范围内Java的编译器以及运行环境会完全依照浮点规范IEEE-754来执行。因此如果想让浮点运算更加精确,而且不会因为不同的硬件平台所执行的结果不一致的话,那就请用关键字strictfp。

可以将一个类、接口以及方法声明为strictfp,但是不允许对接口中的方法以及构造函数声明strictfp关键字。

  1. synchronized线程、同步

synchronized 关键字可以应用于方法或语句块,并为一次只应由一个线程执行的关键代码段提供保护。

synchronized 关键字可防止代码的关键代码段一次被多个线程执行。

如果应用于静态方法,那么,当该方法一次由一个线程执行时,整个类将被锁定。

如果应用于实例方法,那么,当该方法一次由一个线程访问时,该实例将被锁定。

如果应用于对象或数组,当关联的代码块一次由一个线程执行时,对象或数组将被锁定。

一般的用法有:

1、synchronized 方法控制对类成员变量的访问:每个类实例对应一把锁,

每个synchronized 方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态(因为至多只有一个能够获得该类实例对应的锁),从而有效避免了类成员变量的访问冲突(只要所有可能访问类成员变量的方法均被声明为 synchronized)。

在Java中,不光是类实例,每一个类也对应一把锁,这样我们也可将类的静态成员函数声明为 synchronized ,以控制其对类的静态成员变量的访问。

synchronized 方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率,典型地,若将线程类的方法 run() 声明为synchronized ,由于在线程的整个生命期内它一直在运行,因此将导致它对本类任何synchronized 方法的调用都永远不会成功。当然我们可以通过将访问类成员变量的代码放到专门的方法中,将其声明为 synchronized ,并在主方法中调用来解决这一问题,但是 Java 为我们提供了更好的解决办法,那就是 synchronized块。

2、synchronized块。

当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。

尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。

这里的关键之处在于,这个object的对象锁只有一把,一把锁对应一个线程。

  1. transient 短暂

transient 关键字可以应用于类的成员变量,以便指出该成员变量不应在包含它的类实例已序列化时被序列化。

当一个对象被串行化的时候,transient型变量的值不包括在串行化的表示中,然而非transient型的变量是被包括进去的。

Java的serialization提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的对象数据成员,我们不想用serialization机制来保存它。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient。

transient是Java语言的关键字,用来表示一个域不是该对象串行化的一部分。当一个对象被串行化的时候,transient型变量的值不包括在串行化的表示中,然而非transient型的变量是被包括进去的。

  1. volatile 易失

volatile 关键字用于表示可以被多个线程异步修改的成员变量。

注意:volatile 关键字在许多 Java 虚拟机中都没有实现。 volatile 的目标用途是为了确保所有线程所看到的指定变量的值都是相同的。

Volatile修饰的成员变量在每次被线程访问时,都强迫从主内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到主内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。

Java语言规范中指出:

为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才与共享成员变量的原始值对比。

这样当多个线程同时与某个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化。

而volatile关键字就是提示VM:对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。

使用建议:在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,不必使用。

由于使用volatile屏蔽掉了VM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。

Java 语言中的 volatile 变量可以被看作是一种 “程度较轻的 synchronized”;与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized 的一部分。

3、程序控制语句

  1. break 跳出,中断

break 关键字用于提前退出 for、while 或 do 循环,或者在 switch 语句中用来结束 case 块。

break 总是退出最深层的 while、for、do 或 switch 语句。

  1. continue 继续

continue 关键字用来跳转到 for、while 或 do 循环的下一个迭代。

continue 总是跳到最深层 while、for 或 do 语句的下一个迭代。

  1. return 返回

return 关键字会导致方法返回到调用它的方法,从而传递与返回方法的返回类型匹配的值。
如果方法具有非 void 的返回类型,return 语句必须具有相同或兼容类型的参数。
返回值两侧的括号是可选的。

  1. do 运行

do 关键字用于指定一个在每次迭代结束时检查其条件的循环。

do 循环体至少执行一次。
条件表达式后面必须有分号。

  1. while 循环

while 关键字用于指定一个只要条件为真就会重复的循环。

  1. if 如果

if 关键字指示有条件地执行代码块。条件的计算结果必须是布尔值。

if 语句可以有可选的 else 子句,该子句包含条件为 false 时将执行的代码。

包含 boolean 操作数的表达式只能包含 boolean 操作数。

  1. else 否则

else 关键字总是在 if-else 语句中与 if 关键字结合使用。else 子句是可选的,如果 if 条件为 false,则执行该子句。

  1. for 循环

for 关键字用于指定一个在每次迭代结束前检查其条件的循环。

for 语句的形式为 for(initialize; condition; increment) 控件流进入 for 语句时,将执行一次 initialize 语句。
每次执行循环体之前将计算 condition 的结果。如果 condition 为 true,则执行循环体。

每次执行循环体之后,在计算下一个迭代的 condition 之前,将执行 increment 语句。

  1. instanceof 实例

instanceof 关键字用来确定对象所属的类。

  1. switch 观察

switch 语句用于基于某个表达式选择执行多个代码块中的某一个。

switch 条件的计算结果必须等于 byte、char、short 或 int。

case 块没有隐式结束点。break 语句通常在每个 case 块末尾使用,用于退出 switch 语句。

如果没有 break 语句,执行流将进入所有后面的 case 和/或 default 块。

  1. case 返回观察里的结果

case 用来标记 switch 语句中的每个分支。

case 块没有隐式结束点。break 语句通常在每个 case 块末尾使用,用于退出 switch 语句。

如果没有 break 语句,执行流将进入所有后面的 case 和/或 default 块。

  1. default 默认

default 关键字用来标记 switch 语句中的默认分支。

default 块没有隐式结束点。break 语句通常在每个 case 或 default 块的末尾使用,以便在完成块时退出 switch 语句。

如果没有 default 语句,其参数与任何 case 块都不匹配的 switch 语句将不执行任何操作。

4、错误处理

  1. try 捕获异常

try 关键字用于包含可能引发异常的语句块。

每个 try 块都必须至少有一个 catch 或 finally 子句。

如果某个特定异常类未被任何 catch 子句处理,该异常将沿着调用栈递归地传播到下一个封闭 try 块。如果任何封闭 try 块都未捕获到异常,Java 解释器将退出,并显示错误消息和堆栈跟踪信息。

  1. catch 处理异常

catch 关键字用来在 try-catch 或 try-catch-finally 语句中定义异常处理块。

开始和结束标记 { 和 } 是 catch 子句语法的一部分,即使该子句只包含一个语句,也不能省略这两个标记。
每个 try 块都必须至少有一个 catch 或 finally 子句。

如果某个特定异常类未被任何 catch 子句处理,该异常将沿着调用栈递归地传播到下一个封闭 try 块。如果任何封闭 try 块都未捕获到异常,Java 解释器将退出,并显示错误消息和堆栈跟踪信息。

  1. throw 抛出一个异常对象

throw 关键字用于引发异常。

throw 语句将 java.lang.Throwable 作为参数。Throwable 在调用栈中向上传播,直到被适当的 catch 块捕获。
引发非 RuntimeException 异常的任何方法还必须在方法声明中使用 throws 修饰符来声明它引发的异常。

  1. throws 声明一个异常可能被抛出

throws 关键字可以应用于方法,以便指出方法引发了特定类型的异常。

throws 关键字将逗号分隔的 java.lang.Throwables 列表作为参数。

引发非 RuntimeException 异常的任何方法还必须在方法声明中使用 throws 修饰符来声明它引发的异常。
要在 try-catch 块中包含带 throws 子句的方法的调用,必须提供该方法的调用者。

  1. finally

在异常处理机制当中,它的作用就像是人吃饭一样,必须得做的,不论有异常还是没有异常都要执行的代码就可以放到finally块当中去。finally块,必须要配合try块一起使用,不能单独使用,也不能直接和catch块一起使用。

finally 关键字用来定义始终在 try-catch-finally 语句中执行的块。

finally 块通常包含清理代码,用在部分执行 try 块后恢复正常运行。

5、包相关

  1. import 引入

import 关键字使一个包中的一个或所有类在当前 Java 源文件中可见。可以不使用完全限定的类名来引用导入的类。

当多个包包含同名的类时,许多 Java 程序员只使用特定的 import 语句(没有“*”)来避免不确定性。

  1. package 包

package 关键字指定在 Java 源文件中声明的类所驻留的 Java 包。

package 语句(如果出现)必须是 Java 源文件中的第一个非注释性文本。
例:java.lang.Object。
如果 Java 源文件不包含 package 语句,在该文件中定义的类将位于“默认包”中。请注意,不能从非默认包中的类引用默认包中的类。

6、基本类型

  1. boolean 布尔型

boolean 是 Java 原始类型。boolean 变量的值可以是 true 或 false。

boolean 变量只能以 true 或 false 作为值。boolean 不能与数字类型相互转换。

包含 boolean 操作数的表达式只能包含 boolean 操作数。

Boolean 类是 boolean 原始类型的包装对象类。

  1. byte 字节型

byte 是 Java 原始类型。byte 可存储在 [-128, 127] 范围以内的整数值。

Byte 类是 byte 原始类型的包装对象类。它定义代表此类型的值的范围的 MIN_VALUE 和 MAX_VALUE 常量。
Java 中的所有整数值都是 32 位的 int 值,除非值后面有 l 或 L(如 235L),这表示该值应解释为 long。

  1. char 字符型

char 是 Java 原始类型。char 变量可以存储一个 Unicode 字符。

可以使用下列 char 常量:

\b - 空格, \f - 换页, \n - 换行, \r - 回车, \t - 水平制表符, ' - 单引号, " - 双引号, \ - 反斜杠, \xxx - 采用 xxx 编码的 Latin-1 字符。\x 和 \xx 均为合法形式,但可能引起混淆。 \uxxxx - 采用十六进制编码 xxxx 的 Unicode 字符。

Character 类包含一些可用来处理 char 变量的 static 方法,这些方法包括 isDigit()、isLetter()、isWhitespace() 和 toUpperCase()。

char 值没有符号。

  1. double 双精度

double 是 Java 原始类型。double 变量可以存储双精度浮点值。

由于浮点数据类型是实际数值的近似值,因此,一般不要对浮点数值进行是否相等的比较。

Java 浮点数值可代表无穷大和 NaN(非数值)。Double 包装对象类用来定义常量 MIN_VALUE、MAX_VALUE、NEGATIVE_INFINITY、POSITIVE_INFINITY 和 NaN。

  1. float 浮点

float 是 Java 原始类型。float 变量可以存储单精度浮点值。

使用此关键字时应遵循下列规则:

Java 中的浮点文字始终默认为双精度。要指定单精度文字值,应在数值后加上 f 或 F,如 0.01f。

由于浮点数据类型是实际数值的近似值,因此,一般不要对浮点数值进行是否相等的比较。

Java 浮点数值可代表无穷大和 NaN(非数值)。Float 包装对象类用来定义常量 MIN_VALUE、MAX_VALUE、NEGATIVE_INFINITY、POSITIVE_INFINITY 和 NaN。

  1. int 整型

int 是 Java 原始类型。int 变量可以存储 32 位的整数值。

Integer 类是 int 原始类型的包装对象类。它定义代表此类型的值的范围的 MIN_VALUE 和 MAX_VALUE 常量。

Java 中的所有整数值都是 32 位的 int 值,除非值后面有 l 或 L(如 235L),这表示该值应解释为 long。

  1. long 长整型

long 是 Java 原始类型。long 变量可以存储 64 位的带符号整数。

Long 类是 long 原始类型的包装对象类。它定义代表此类型的值的范围的 MIN_VALUE 和 MAX_VALUE 常量。

Java 中的所有整数值都是 32 位的 int 值,除非值后面有 l 或 L(如 235L),这表示该值应解释为 long。

  1. short 短整型

short 是 Java 原始类型。short 变量可以存储 16 位带符号的整数。

Short 类是 short 原始类型的包装对象类。它定义代表此类型的值的范围的 MIN_VALUE 和 MAX_VALUE 常量。

Java 中的所有整数值都是 32 位的 int 值,除非值后面有 l 或 L(如 235L),这表示该值应解释为 long。

  1. null 空

null 是 Java 的保留字,表示无值。

将 null 赋给非原始变量相当于释放该变量先前所引用的对象。

不能将 null 赋给原始类型(byte、short、int、long、char、float、double、boolean)变量。

  1. true 真

true 关键字表示 boolean 变量的两个合法值中的一个。

  1. false 假

false 关键字代表 boolean 变量的两个合法值之一。

7、变量引用

  1. super 父类,超类

super 关键字用于引用使用该关键字的类的超类。

作为独立语句出现的 super 表示调用超类的构造方法。
super.() 表示调用超类的方法。只有在如下情况中才需要采用这种用法:要调用在该类中被重写的方法,以便指定应当调用在超类中的该方法。

  1. this 本类

this 关键字用于引用当前实例。
当引用可能不明确时,可以使用 this 关键字来引用当前的实例。

  1. void 无返回值

void 关键字表示 null 类型。
void 可以用作方法的返回类型,以指示该方法不返回值。

8、保留字

正确识别java语言的关键字(keyword)和保留字(reserved word)是十分重要的。Java的关键字对java的编译器有特殊的意义,他们用来表示一种数据类型,或者表示程序的结构等。保留字是为java预留的关键字,他们虽然现在没有作为关键字,但在以后的升级版本中有可能作为关键字。
识别java语言的关键字,不要和其他语言如c/c++的关键字混淆。
const和goto是java的保留字。 所有的关键字都是小写

  1. goto 跳转

goto 保留关键字,但无任何作用。结构化程序设计完全不需要 goto 语句即可完成各种流程,而 goto 语句的使用往往会使程序的可读性降低,所以 Java 不允许 goto 跳转。

  1. const 静态

const 保留字,是一个类型修饰符,使用const声明的对象不能更新。与final某些类似。

Java字节码总结

目录

1. 概述

学过Java的都知道,Java从一开始就打着平台无关性的旗号,说“一次编写,到处运行”,其实说到无关性,Java平台还有另外一个无关性那就是语言无关性。实现语言无关性,那么Java体系中的class文件结构或者说是字节码就显得相当重要了,其实Java从刚开始的时候就有两套规范,一个是Java语言规范,另外一个是Java虚拟机规范,Java语言规范只是规定了Java语言相关的约束以及规则,而虚拟机规范则才是真正从跨平台的角度去设计的。

Java代码要想执行,需要先被编译成Class文件,即使Java字节码文件,然后才能够在JVM上执行。

那么现在我们简单的了解下Java字节码究竟是什么。这种类汇编的指令有是如何在JVM上面执行的呢?

在Class文件中,Java方法里的方法体,也就是代表着一个Java源码程序中程序的部分存储在方法表集合的Code属性中。存储在Code属性中的是字节码,也就是编译后的程序。Java虚拟机的指令由两部分组成,首先是一个字节长度、代表某种含义的数字(即操作码),在操作码后面跟着零个或多个代表这个操作所需的参数(即操作数)。由于Java虚拟机采用的是面向操作数栈而不是寄存器的架构,所以大多数指令不包含操作数,只有一个操作码。

操作码的长度只有一个字节,这就限制了操作码的个数不超过256个。同时,Class文件格式放弃了编译后代码的操作数长度对齐,这就意味着虚拟机处理那些超过一个字节的数据时,不得不在运行时从字节中重建出具体数据的结构。比如,如果要将一个两个字节长的无符号整数使用两个无符号字节存储起来分别是byte1和byte2,那么就需要这样构造出原始的无符号整数:

(byte1<<8) | byte2

操作数的数量以及长度,取决于操作码,若一个操作数长度超过了一个字节,将会以Big-Endian顺序存储(高位在前字节码)。

这样会在某种程度上导致执行字节码时损失一些性能。但这样做也有好处,那就是由于不需要对齐,省去了中间的填充与间隔符号;用一个字节来表示操作码,也是为了获得短小的编译代码。这样就尽可能的减少了编译后的代码的长度,非常适合网络传输。

如果不考虑异常处理的话,那么Java虚拟机的解释器可以使用下面的伪代码作为基本的执行模型来理解:

1
2
3
4
5
6
7
8

do{
自动计算PC寄存器的值加1;
根据PC寄存器的指示位置,从字节码流中取出操作码;
if(字节码存在操作数) 从字节码流中取出操作数;
执行操作码所定义的操作;
}while(字节码长度>0);

2. 举个栗子

可能大家现在有点不明觉厉哦,那么咱们先上代码,看看是个什么情况。

首先是一个简单的在控制台打印“Hello”的Java代码,如下:

1
2
3
4
5
6
7

public class SimpleClass {
public void sayHello() {
System.out.println("Hello");
}
}

然后我们使用javac SimpleClass.java命令,编译该Java代码,生成相应的Class文件。然后,继续在终端中使用javap -verbose SimpleClass,打印Class文件中的内容。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

Classfile /C:/Users/yalechen/Desktop/JVM/Java Byte Code/samples/SimpleClass.class
Last modified 2016-8-16; size 400 bytes
MD5 checksum 92d47034320261dac69592f3c3e33c2e
Compiled from "SimpleClass.java"
public class SimpleClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#14 // java/lang/Object."<init>":()V
#2 = Fieldref #15.#16 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #17 // Hello
#4 = Methodref #18.#19 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #20 // SimpleClass
#6 = Class #21 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 sayHello
#12 = Utf8 SourceFile
#13 = Utf8 SimpleClass.java
#14 = NameAndType #7:#8 // "<init>":()V
#15 = Class #22 // java/lang/System
#16 = NameAndType #23:#24 // out:Ljava/io/PrintStream;
#17 = Utf8 Hello
#18 = Class #25 // java/io/PrintStream
#19 = NameAndType #26:#27 // println:(Ljava/lang/String;)V
#20 = Utf8 SimpleClass
#21 = Utf8 java/lang/Object
#22 = Utf8 java/lang/System
#23 = Utf8 out
#24 = Utf8 Ljava/io/PrintStream;
#25 = Utf8 java/io/PrintStream
#26 = Utf8 println
#27 = Utf8 (Ljava/lang/String;)V
{
public SimpleClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0

public void sayHello();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 6: 0
line 7: 8
}
SourceFile: "SimpleClass.java"

我们从上往下依次解析。

  1. 首先是该Class文件的整体信息介绍,包括文件所在路径、最近修改时间、文件大小、MD5校验值、从哪个Java编译而来。接下来才是正文。
    
  2. 我们一步一步来:
    

1) 该类的声明

2) Minor version,表示Class文件的次版本号。Major version,表示Class文件的主版本号。major_version和minor_version主要用来表示当前的虚拟机是否接受当前这种版本的Class文件。不同版本的Java编译器编译的Class文件对应的版本是不一样的。高版本的虚拟机支持低版本的编译器编译的 Class文件结构。比如Java SE 6.0对应的虚拟机支持Java SE 5.0的编译器编译的Class文件结构,反之则不行。

3) Flags,是该类的一些标识,包括访问权限(public、private、protected),父类、实现的接口等等。

4) Constant pool,常量池。以下的一些都是定义的常量。这个算是Java代码编译上的一种优化,把一些引用明确的变量、方法、类等,转换成直接引用。

在能够唯一确定方法的直接引用的时候,虚拟机会将常量表里的符号引用转换为直接引用。但是如果方法是动态绑定的,也就是说在编译期我们并不知道使用哪个方法(或者叫不知道使用方法的哪个版本),那么这个时候就需要在运行时才能确定哪个版本的方法将被调用,这个时候才能将符号引用转换为直接引用。这个问题提到的多个版本的方法在java中的重载和多态重写问题息息相关。

具体的定义,比如:

#1 = Methodref          #6.#14      // java/lang/Object."<init>":()V

#1是定义的引用的别称;

Methodref是该引用的具体类别,还有好几种不同的类别,之后会提及;

#6.#14是具体的引用名称,不过此时的引用名称也是有下面定义的引用别称来表示的。后面的双斜杠注释表示对引用的解释。

5) 花括号中的便是SimpleClasss类的内容了。其中是一个一个的方法定义。

6) public SimpleClass();这个是构造方法的声明。简直就是Java代码有没有。

7) descriptor: ()V描述。()V,看到括号里面没有参数,V便是Void,所以这是个无参无返回值的方法。

8) flags的含义跟跟上面提及的一样。

9) code属性,表示的是方法的具体内容。

10)stack、locals以及args_size,读入栈深度建立符合要求的操作数栈,读入局部变量大小建立符合要求的局部变量表,根据参数个数向局部变量表中依序加入参数(第一个参数是引用当前对象的this,所以空参数列表的参数数也是1)

11)LineNumberTable,是指每一个java字节码指令对应java代码文件中的第几行,以方便定位。

12)同理,sayHello方法也是如此道理。

13)SourceFile,这是源代码。

不知大家看懂了多少,先凑合着来,慢慢理清头绪。  

3. Class文件结构

首先需要明确如下几点:

1)Class文件是由8个字节为基础的字节流构成的,这些字节流之间都严格按照规定的顺序排列,并且字节之间不存在任何空隙,对于超过8个字节的数据,将按照Big-Endian的顺序存储的,也就是说高位字节存储在低的地址上面,而低位字节存储到高地址上面,其实这也是class文件要跨平台的关键,因为 PowerPC架构的处理采用Big-Endian的存储顺序,而x86系列的处理器则采用Little-Endian的存储顺序,因此为了Class文件在各中处理器架构下保持统一的存储顺序,虚拟机规范必须对起进行统一。

2) Class文件结构采用类似C语言的结构体来存储数据的,主要有两类数据项,无符号数和表,无符号数用来表述数字,索引引用以及字符串等,比如u1,u2,u4,u8分别代表1个字节,2个字节,4个字节,8个字节的无符号数,而表是有多个无符号数以及其它的表组成的复合结构。

明确了上面的两点以后,我们接下来后来看看Class文件中按照严格的顺序排列的字节流都具体包含些什么数据:

javabytecode1.png

在看上图的时候,有一点我们需要注意,比如cp_info,cp_info表示常量池,上图中用 constant_pool[constant_pool_count-1]的方式来表示常量池有constant_pool_count-1个常量,它这里是采用数组的表现形式,但是大家不要误以为所有的常量池的常量长度都是一样的,其实这个地方只是为了方便描述采用了数组的方式,但是这里并不像编程语言那里,一个int型的数组,每个int长度都一样。明确了这一点以后,我们在回过头来看看上图中每一项都具体代表了什么含义。

1)u4 magic 表示魔数,并且魔数占用了4个字节,魔数到底是做什么的呢?它其实就是表示一下这个文件的类型是一个Class文件,而不是一张JPG图片,或者AVI的电影。而Class文件对应的魔数是0xCAFEBABE.

2)u2 minor_version 表示Class文件的次版本号,并且此版本号是u2类型的无符号数表示。

3)u2 major_version 表示Class文件的主版本号,并且主版本号是u2类型的无符号数表示。major_version和minor_version主要用来表示当前的虚拟机是否接受当前这种版本的Class文件。不同版本的Java编译器编译的Class文件对应的版本是不一样的。高版本的虚拟机支持低版本的编译器编译的 Class文件结构。比如Java SE 6.0对应的虚拟机支持Java SE 5.0的编译器编译的Class文件结构,反之则不行。

4)u2 constant_pool_count 表示常量池的数量。这里我们需要重点来说一下常量池是什么东西,请大家不要与Jvm内存模型中的运行时常量池混淆了,Class文件中常量池主要存储了字面量以及符号引用,其中字面量主要包括字符串,final常量的值或者某个属性的初始值等等,而符号引用主要存储类和接口的全限定名称,字段的名称以及描述符,方法的名称以及描述符,这里名称可能大家都容易理解,至于描述符的概念,放到下面说字段表以及方法表的时候再说。另外大家都知道Jvm的内存模型中有堆,栈,方法区,程序计数器构成,而方法区中又存在一块区域叫运行时常量池,运行时常量池中存放的东西其实也就是编译器长生的各种字面量以及符号引用,只不过运行时常量池具有动态性,它可以在运行的时候向其中增加其它的常量进去,最具代表性的就是String的intern方法。

5)cp_info 表示常量池,这里面就存在了上面说的各种各样的字面量和符号引用。放到常量池的中数据项在The Java Virtual Machine Specification Java SE 7 Edition 中一共有14个常量,每一种常量都是一个表,并且每种常量都用一个公共的部分tag来表示是哪种类型的常量。

下面分别简单描述一下具体细节等到后面的实例中我们再细化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

CONSTANT_Utf8_info tag标志位为1, UTF-8编码的字符串

CONSTANT_Integer_info tag标志位为3,整形字面量

CONSTANT_Float_info tag标志位为4,浮点型字面量

CONSTANT_Long_info tag标志位为5,长整形字面量

CONSTANT_Double_info tag标志位为6,双精度字面量

CONSTANT_Class_info tag标志位为7,类或接口的符号引用

CONSTANT_String_info tag标志位为8,字符串类型的字面量

CONSTANT_Fieldref_info tag标志位为9, 字段的符号引用

CONSTANT_Methodref_info tag标志位为10,类中方法的符号引用

CONSTANT_InterfaceMethodref_info tag标志位为11, 接口中方法的符号引用

CONSTANT_NameAndType_info tag 标志位为12,字段和方法的名称以及类型的符号引用

可参考下图。

javabytecode2.png

而这是在Java字节码中常出现的各种常量类型的字符描述符:

javabytecode3.png

6)u2 access_flags 表示类或者接口的访问信息,具体如下图所示:

javabytecode4.png

7)u2 this_class 表示类的常量池索引,指向常量池中CONSTANT_Class_info的常量

8)u2 super_class 表示超类的索引,指向常量池中CONSTANT_Class_info的常量

9)u2 interface_counts 表示接口的数量

10)u2 interface[interface_counts]表示接口表,它里面每一项都指向常量池中CONSTANT_Class_info常量

11)u2 fields_count 表示类的实例变量和类变量的数量

12)field_info fields[fields_count]表示字段表的信息,其中字段表的结构如下图所示:

javabytecode5.png

上图中access_flags表示字段的访问表示,比如字段是public,private,protect 等,name_index表示字段名称,指向常量池中类型是CONSTANT_UTF8_info的常量,descriptor_index表示字段的描述符,它也指向常量池中类型为 CONSTANT_UTF8_info的常量,attributes_count表示字段表中的属性表的数量,而属性表是则是一种用与描述字段,方法以及类的属性的可扩展的结构,不同版本的Java虚拟机所支持的属性表的数量是不同的。

javabytecode6.png

13)u2 methods_count表示方法表的数量

14)method_info 表示方法表,方法表的具体结构如下图所示:

javabytecode7.png

其中access_flags表示方法的访问表示,name_index表示名称的索引,descriptor_index表示方法的描述符,attributes_count以及attribute_info类似字段表中的属性表,只不过字段表和方法表中属性表中的属性是不同的,比如方法表中就Code属性,表示方法的代码,而字段表中就没有Code属性。Code属性的结构如下图:

javabytecode8.png

attribute_name_index指向常量池中值为Code的常量;

attribute_length表示Code属性表的长度(这里需要注意的时候长度不包括attribute_name_index和attribute_length的6个字节的长度);

max_stack表示最大栈深度,虚拟机在运行时根据这个值来分配栈帧中操作数的深度,而max_locals代表了局部变量表的存储空间;

max_locals的单位为slot,slot是虚拟机为局部变量分配内存的最小单元,在运行时,对于不超过32位类型的数据类型,比如 byte,char,int等占用1个slot,而double和Long这种64位的数据类型则需要分配2个slot,另外max_locals的值并不是所有局部变量所需要的内存数量之和,因为slot是可以重用的,当局部变量超过了它的作用域以后,局部变量所占用的slot就会被重用;

code_length代表了字节码指令的数量,而code表示的时候字节码指令,从上图可以知道code的类型为u1,一个u1类型的取值为0x00-0xFF,对应的十进制为0-255,目前虚拟机规范已经定义了200多条指令;

exception_table_length以及exception_table分别代表方法对应的异常信息;

attributes_count和attribute_info分别表示了Code属性中的属性数量和属性表,从这里可以看出Class的文件结构中,属性表是很灵活的,它可以存在于Class文件,方法表,字段表以及Code属性中。

15) attribute_count表示属性表的数量,说到属性表,我们需要明确以下几点:

  • 属性表存在于Class文件结构的最后,字段表,方法表以及Code属性中,也就是说属性表中也可以存在属性表

  • 属性表的长度是不固定的,不同的属性,属性表的长度是不同的

16)LineNumberTable,用于描述java源代码的行号和字节码行号的对应关系,它不是运行时必需的属性,如果通过-g:none的编译器参数来取消生成这项信息的话,最大的影响就是异常发生的时候,堆栈中不能显示出出错的行号,调试的时候也不能按照源代码来设置断点,接下来我们再看一下LineNumberTable的结构如下图所示:

javabytecode9.png

其中attribute_name_index表示常量池的索引,attribute_length表示属性长度,而start_pc和 line_number分表表示字节码的行号和源代码的行号。

17)SourceFile,SourceFile的结构如下图所示:

javabytecode10.png

其中attribute_length为属性的长度,sourcefile_index指向常量池中值为源代码文件名称的常量。

4. 指令

以下是具体使用到的Java字节码指令集。

1) 字节码与数据类型

在Java虚拟机的指令集中,大多数的指令都包含了操作所对应的数据类型信息。比如iload指令表示从局部变量表中加载int型数据到操作数栈中,而fload表示加载float类型的数据。不过,这两条指令再虚拟机的内部可能是由同一段代码来实现的,但在class文件中必须有自己的操作码。

我们已经知道Java指令的长度只有一个字节,这就限制了指令集的大小。如果每个指令都像上面两个指令那样包含所有的数据类型,那么就有可能导致指令过多。因此,Java虚拟机的指令集对于特定的操作只提供了有限的类型相关指令去支持它。比如,大多数指令没有支持整数类型byte、char和short,甚至没有指令支持boolean类型。

这些指令中都有特殊的字符来表示专门支持的类型:i代表int类型,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表Reference。

这里仅仅介绍一下指令的种类以及作用,并不会过多的介绍各个指令的含义以及使用,需要的话可以查看《Java虚拟机规范(Java SE 7版)》。

2) 加载和存储指令

加载指令用于将局部变量表中的数据传送到操作数栈中,而存储指令用于将操作数栈中的结果传送到局部变量表中。这类指令包括如下几种:

将一个局部变量加载到操作栈,比如iload、iload、fload、fload、lload、lload、dload、dload、aload、aload

将一个数值从操作数栈存储到局部变量表,比如istore、istore、lstore、lstore、fstore、fstore、dstore、dstore、astore、astore

将一个常量加载到操作数栈,比如bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_ml、iconst_、lconst_、fconst_、dconst_

扩充局部变量表的访问索引的指令:wide;

上面中带尖括号的指令实际是一组指令。比如iload,代表了iload_1、iload_2和iload_3。这几组指令是某个带操作数的指令(比如iload)的特殊形式,它们省略了操作数,不过操作数隐含在指令中。

3) 运算指令

运算或算术指令用于对一个或两个操作数栈上的值进行某种特定的运算,并将结果存入栈顶。大体上可以分为两种,对整数进行运算的指令和对浮点数进行运算的指令。不过,由于没有支持byte、short、char和boolean的算术指令,对于这些数据的运算,会把它们转化为int类型进行运算。指令列出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

加法指令:iadd、ladd、fadd、dadd;

减法指令:isub、lsub、fsub、dsub;

乘法指令:imul、lmul、fmul、dmul;

除法指令:idiv、ldiv、fdiv、ddiv;

求余指令:irem、lrem、frem、drem;

取反指令:ineg、lneg、fneg、dneg;

位移指令:ishl、ishr、iushr、lshl、lshr、lushr;

按位或指令:ior、lor;

按位与指令:iand、land;

按位异或指令:ixor、lxor;

局部变量自增指令:iinc;

比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp;

4) 类型转换指令

类型转换指令用来将两种不同类型进行转换,这些转换操作一般用于实现代码中的显示类型转换操作,或者前面提到的字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。

虚拟机直接支持宽化类型转换,即小范围类型向大范围类型的安全转换,不需要显示的转换指令。

但是处理窄化类型转换时,必须显示使用转换指令来完成,这些指令包括:i2b、i2c、i2s、l2i、f2l、d2i、d2l和d2f。这些指令可能会导致数值的精度丢失。

5) 对象创建与访问指令

虽然类实例和数组都是对象,但是虚拟机创建类对象和数组的指令是不同的。对象创建后,就可以通过对象访问指令获取对象实例或者数组实例中的字段或数组元素,指令如下:

创建类实例的指令:new;

创建数组的指令:newarray、anewarray、multianewarray;

访问类字段和实例字段的指令:getfield、putfield、getstatic、putstatic;

把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload;

将一个操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore;

取数组长度的指令:arraylength;

检查类实例类型的指令:instanceof、checkcast;

6) 操作数栈管理指令

就像操作一个普通的栈一样,Java虚拟机提供了一些用于直接操作操作数栈的指令,包括:

将操作数栈的栈顶一个或两个元素出栈:pop、pop2;

复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2;

将栈顶最顶端的两个数值互换:swap;

7) 控制转移指令

控制转移指令可以让Java虚拟机有条件或无条件的从指定的位置指令而不是控制转移指令的下一条指令继续执行,可以理解为控制转移指令改变了PC寄存器的值。指令如下:

条件分支:ifeq、iflt、ifle、ifgt、ifge、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、、if_icmpge、if_acmpeq和if_acmpne;

复合条件分支:tableswitch、lookupswitch;

无条件分支:goto、goto_w、jsr、jsr_w、ret;

8) 方法调用和返回指令

这里仅仅列出5条用于方法调用的指令:

invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式;

invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用;

invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法;

invokestatic指令用于调用类方法(static方法);

invokedynamic指令用于在运行时动态解析出调用点限定符索引用的方法,并执行方法,前面4条指令的分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的;

方法调用指令与类型无关,但是方法返回指令是根据返回值的类型区分的,包括ireturn、lreturn、freturn、dreturn和areturn,另外还有一个return指令供声明为void的方法、实例初始化方法以及类和接口的类初始化方法使用。

9) 异常处理指令

在Java程序中显式抛出异常的操作(throw语句)都是由athrow指令来实现的,除了用throw语句显式抛出异常外,Java虚拟机规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。

而在Java虚拟机中,处理异常(catch语句)不是由字节码指令来完成的,而是采用异常表来完成的。

10) 同步指令

Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的。

方法级的同步是隐式的,即不需要通过字节码指令来控制,它实现在方法调用和返回操作中。虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法。当方法调用时,调用指令就会去检查方法的ACC_SYNCHRONIZED访问标志是否被设置了,如果设置,执行线程就要求持有管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个方法在执行期间发生了异常,并在方法中无法处理此异常,那么这个同步方法所持有的管程将在异常抛出后自动释放。

同步一段指令集序列通常是由Java语言中的synchronized语句块表示的,Java虚拟机的指令集中有monitorenter和monitorexit指令来支持synchronized关键字的语义。正确实现synchronized关键字需要Javac编译器和Java虚拟机两者共同协作。编译器必须保证每个monitorenter指令都有对应的monitorexit指令。

5. 再看几个栗子

例1

Java源代码如下:

1
2
3
4
5
6
7
8
9
10
11

public class TestDemo {
public static int minus(int x){
return -x;
}
public static void main(String[] args) {
int x = 5;
int y = minus(x);
}
}

Javap–verbose TestDemo之后,得到Java字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

Classfile /C:/Users/yalechen/Desktop/JVM/Java Byte Code/samples/TestDemo/TestDemo.class
Last modified 2016-8-16; size 342 bytes
MD5 checksum 77ff8854473da6d63caf8a5347bb3434
Compiled from "TestDemo.java"
public class TestDemo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Methodref #3.#16 // TestDemo.minus:(I)I
#3 = Class #17 // TestDemo
#4 = Class #18 // java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 minus
#10 = Utf8 (I)I
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 TestDemo.java
#15 = NameAndType #5:#6 // "<init>":()V
#16 = NameAndType #9:#10 // minus:(I)I
#17 = Utf8 TestDemo
#18 = Utf8 java/lang/Object
{
public TestDemo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V V表示Void,()表示无参数,冒号之前表示的是方法名
4: return
LineNumberTable:
line 3: 0

public static int minus(int);
descriptor: (I)I //描述符描述的参数列表和返回类型.I表示int
flags: ACC_PUBLIC, ACC_STATIC //访问权限
Code:
stack=1, locals=1, args_size=1
0: iload_0 //将slot0压入栈顶,也就是传入的参数
1: ineg //将栈顶的值弹出取负后压回栈顶
2: ireturn
LineNumberTable:
line 5: 0

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code: //确定可以访问后进入Code属性表执行命令
stack=1, locals=3, args_size=1 //读入栈深度建立符合要求的操作数栈,读入局部变量大小建立符合要求的局部变量表,根据参数个数向局部变量表中依序加入参数(第一个参数是引用当前对象的this,所以空参数列表的参数数也是1)
0: iconst_5 //前面 0 表示标识。将整数5压入栈顶
1: istore_1 //将栈顶整数值存入局部变量表的slot1(slot0是参数this)到这为止,int x = 5;

2: iload_1 //将slot1压入栈顶
3: invokestatic #2 // Method minus:(I)I 调用静态累方法minus。二进制invokestatic方法用于调用静态方法,参数是根据常量池中已经转换为直接引用的常量,意即minus函数在方法区中的地址,找到这个地址调用函数,向其中加入的参数是栈顶的值
6: istore_2 //此时的栈顶元素是经过了 minus 方法处理过了的
7: return
LineNumberTable:
line 8: 0
line 9: 2
line 10: 7
}
SourceFile: "TestDemo.java"

例2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

//测试重载机制
public class TestDemo1 {
static class Human{
}
static class Man extends Human{

}
static class Woman extends Human{

}
public void sayHello(Human human) {
System.out.println("hello human");
}
public void sayHello(Man man) {
System.out.println("hello man");
}
public void sayHello(Woman woman) {
System.out.println("hello woman");
}
public static void main(String[] args) {
TestDemo1 demo = new TestDemo1();
Human man = new Man();
Human woman = new Woman();
demo.sayHello(man);
demo.sayHello(woman);
}
}

Java字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151

Classfile /C:/Users/yalechen/Desktop/JVM/Java Byte Code/samples/TestDemo/TestDemo1.class
Last modified 2016-8-16; size 883 bytes
MD5 checksum 5a1df45f23bd666f3c6c716e50265345
Compiled from "TestDemo1.java"

public class TestDemo1 //重载
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #14.#32 // java/lang/Object."<init>":()V
#2 = Fieldref #33.#34 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #35 // hello human
#4 = Methodref #36.#37 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = String #38 // hello man
#6 = String #39 // hello woman
#7 = Class #40 // TestDemo1
#8 = Methodref #7.#32 // TestDemo1."<init>":()V
#9 = Class #41 // TestDemo1$Man
#10 = Methodref #9.#32 // TestDemo1$Man."<init>":()V
#11 = Class #42 // TestDemo1$Woman
#12 = Methodref #11.#32 // TestDemo1$Woman."<init>":()V
#13 = Methodref #7.#43 // TestDemo1.sayHello:(LTestDemo1$Human;)V
#14 = Class #44 // java/lang/Object
#15 = Utf8 Woman
#16 = Utf8 InnerClasses
#17 = Utf8 Man
#18 = Class #45 // TestDemo1$Human
#19 = Utf8 Human
#20 = Utf8 <init>
#21 = Utf8 ()V
#22 = Utf8 Code
#23 = Utf8 LineNumberTable
#24 = Utf8 sayHello
#25 = Utf8 (LTestDemo1$Human;)V
#26 = Utf8 (LTestDemo1$Man;)V
#27 = Utf8 (LTestDemo1$Woman;)V
#28 = Utf8 main
#29 = Utf8 ([Ljava/lang/String;)V
#30 = Utf8 SourceFile
#31 = Utf8 TestDemo1.java
#32 = NameAndType #20:#21 // "<init>":()V
#33 = Class #46 // java/lang/System
#34 = NameAndType #47:#48 // out:Ljava/io/PrintStream;
#35 = Utf8 hello human
#36 = Class #49 // java/io/PrintStream
#37 = NameAndType #50:#51 // println:(Ljava/lang/String;)V
#38 = Utf8 hello man
#39 = Utf8 hello woman
#40 = Utf8 TestDemo1
#41 = Utf8 TestDemo1$Man
#42 = Utf8 TestDemo1$Woman
#43 = NameAndType #24:#25 // sayHello:(LTestDemo1$Human;)V
#44 = Utf8 java/lang/Object
#45 = Utf8 TestDemo1$Human
#46 = Utf8 java/lang/System
#47 = Utf8 out
#48 = Utf8 Ljava/io/PrintStream;
#49 = Utf8 java/io/PrintStream
#50 = Utf8 println
#51 = Utf8 (Ljava/lang/String;)V
{
public TestDemo1(); //构造方法
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0

public void sayHello(TestDemo1$Human);
descriptor: (LTestDemo1$Human;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello human
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 11: 0
line 12: 8

public void sayHello(TestDemo1$Man);
descriptor: (LTestDemo1$Man;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #5 // String hello man
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 14: 0
line 15: 8

public void sayHello(TestDemo1$Woman);
descriptor: (LTestDemo1$Woman;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String hello woman
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 17: 0
line 18: 8

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: new #7 // class TestDemo1
3: dup
4: invokespecial #8 // Method "<init>":()V
7: astore_1
8: new #9 // class TestDemo1$Man
11: dup
12: invokespecial #10 // Method TestDemo1$Man."<init>":()V
15: astore_2
16: new #11 // class TestDemo1$Woman
19: dup
20: invokespecial #12 // Method TestDemo1$Woman."<init>":()V
23: astore_3
24: aload_1
25: aload_2
26: invokevirtual #13 // Method sayHello:(LTestDemo1$Human;)V
29: aload_1
30: aload_3
31: invokevirtual #13 // Method sayHello:(LTestDemo1$Human;)V
34: return
LineNumberTable: //与源代码中的各行代码进行相互匹配
line 20: 0
line 21: 8
line 22: 16
line 23: 24
line 24: 29
line 25: 34
}
SourceFile: "TestDemo1.java"
InnerClasses:
static #15= #11 of #7; //Woman=class TestDemo1$Woman of class TestDemo1
static #17= #9 of #7; //Man=class TestDemo1$Man of class TestDemo1
static #19= #18 of #7; //Human=class TestDemo1$Human of class TestDemo1

例3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

/*重写*/
public class TestDemo2 {
static class Human{
public void sayHello() {
System.out.println("hello human");
}
}
static class Man extends Human{
public void sayHello() {
System.out.println("hello man");
}
}
static class Woman extends Human{
public void sayHello() {
System.out.println("hello woman");
}
}

public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
}
}

Java字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

Classfile /C:/Users/yalechen/Desktop/JVM/Java Byte Code/samples/TestDemo/TestDemo2.class
Last modified 2016-8-16; size 464 bytes
MD5 checksum 2183e6c960f34e773da47dac5f8d717a
Compiled from "TestDemo2.java"
public class TestDemo2
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #8.#22 // java/lang/Object."<init>":()V
#2 = Class #23 // TestDemo2$Man
#3 = Methodref #2.#22 // TestDemo2$Man."<init>":()V
#4 = Class #24 // TestDemo2$Woman
#5 = Methodref #4.#22 // TestDemo2$Woman."<init>":()V
#6 = Methodref #12.#25 // TestDemo2$Human.sayHello:()V
#7 = Class #26 // TestDemo2
#8 = Class #27 // java/lang/Object
#9 = Utf8 Woman
#10 = Utf8 InnerClasses
#11 = Utf8 Man
#12 = Class #28 // TestDemo2$Human
#13 = Utf8 Human
#14 = Utf8 <init>
#15 = Utf8 ()V
#16 = Utf8 Code
#17 = Utf8 LineNumberTable
#18 = Utf8 main
#19 = Utf8 ([Ljava/lang/String;)V
#20 = Utf8 SourceFile
#21 = Utf8 TestDemo2.java
#22 = NameAndType #14:#15 // "<init>":()V
#23 = Utf8 TestDemo2$Man
#24 = Utf8 TestDemo2$Woman
#25 = NameAndType #29:#15 // sayHello:()V
#26 = Utf8 TestDemo2
#27 = Utf8 java/lang/Object
#28 = Utf8 TestDemo2$Human
#29 = Utf8 sayHello
{
public TestDemo2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class TestDemo2$Man
3: dup
4: invokespecial #3 // Method TestDemo2$Man."<init>":()V
7: astore_1
8: new #4 // class TestDemo2$Woman
11: dup
12: invokespecial #5 // Method TestDemo2$Woman."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #6 // Method TestDemo2$Human.sayHello:()V
20: aload_2
21: invokevirtual #6 // Method TestDemo2$Human.sayHello:()V
24: return
LineNumberTable:
line 21: 0
line 22: 8
line 23: 16
line 24: 20
line 25: 24
}
SourceFile: "TestDemo2.java"
InnerClasses:
static #9= #4 of #7; //Woman=class TestDemo2$Woman of class TestDemo2
static #11= #2 of #7; //Man=class TestDemo2$Man of class TestDemo2
static #13= #12 of #7; //Human=class TestDemo2$Human of class TestDemo2

可以从例2和例3看出,无论是重载还是重写,都是二进制指令invokevirtual调用了sayHello方法来执行的。

在重载中,程序调用的是参数实际类型不同的方法,但是虚拟机最终分派了相同外观类型(静态类型)的方法,这说明在重载的过程中虚拟机在运行的时候是只看参数的外观类型(静态类型)的,而这个外观类型(静态类型)是在编译的时候就已经确定的,和虚拟机没有关系。这种依赖静态类型来做方法的分配叫做静态分派。

在重写中,程序调用的是不同实际类型的同名方法,虚拟机依据对象的实际类型去寻找是否有这个方法,如果有就执行,如果没有去父类里找,最终在实际类型里找到了这个方法,所以最终是在运行期动态分派了方法。在编译的时候我们可以看到字节码指示的方法都是一样的符号引用,但是运行期虚拟机能够根据实际类型去确定出真正需要的直接引用。这种依赖实际类型来做方法的分配叫做动态分派。得益于java虚拟机的动态分派会在分派前确定对象的实际类型,面向对象的多态性才能体现出来。

R语言实现PageRank

前言

当今,一个算法能够支撑起一个商业帝国,这句话还真不是吹的,你看Google就知道了。Google当年就是靠PageRank雄起的,将网页搜索业务做得风生水起。

而今天,我们就来看看如何使用R语言来实现PageRank算法的。

目录

1.PageRank的原理

2.PageRank实现步骤

3.R代码实现

1.PageRank的原理

其实说白了,PageRank原理就是:

PageRank让链接来”投票”

一个页面的“得票数”由所有链向它的页面的重要性来决定。到一个页面的超链接相当于对该页投一票。

一个页面的PageRank是由所有链向它的页面(“链入页面”)的重要性经过递归算法得到的。

一个有较多链入的页面会有较高的等级,相反如果一个页面没有任何链入页面,那么它没有等级。

PageRank的计算基于以下两个基本假设

数量假设:如果一个页面节点接收到的其他网页指向的入链数量越多,那么这个页面越重要。

质量假设:指向页面A的入链质量不同,质量高的页面会通过链接向其他页面传递更多的权重。所以越是质量高的页面指向页面A,则页面A越重要。

要提高PageRank有3个要点

  • 反向链接数
  • 反向链接是否来自PageRank较高的页面
  • 反向链接源页面的链接数

基于以上特点,我们再来看看PageRank的基本公式吧。

pr

2.PageRank实现步骤

  • 构造邻接矩阵

    列:源页面

    行:目标页面

  • 概率矩阵(转移矩阵)

    计算公式在这儿体现跟连接数有关的

  • 求开率矩阵的特征值

    一般使用的是迭代的算法。当然也可以使用R中的API直接给出特征值。
    而是用迭代的话,在一轮更新页面PageRank得分的计算中,每个页面将其当前的PageRank值平均分配到本页面包含的出链上,这样每个链接即获得了相应的权值。而每个页面将所有指向本页面的入链所传入的权值求和,即可得到新的PageRank得分。当每个页面都获得了更新后的PageRank值,就完成了一轮PageRank计算。

3.R代码实现

分别用下面3种方式实现PageRank:

  • 未考虑阻尼系统的情况
  • 包括考虑阻尼系统的情况
  • 直接用R的特征值计算函数

但是,在这之前,我们还有新建一个page.csv文件,方便我们后面的实现说明。

1
2
3
4
5
6
7
1,2
1,3
1,4
2,3
2,4
3,4
4,2

1)未考虑阻尼系统的情况

以下是函数定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#构建邻接矩阵
adjacencyMatrix<-function(pages){
n<-max(apply(pages,2,max))
A <- matrix(0,n,n)
for(i in 1:nrow(pages)) A[pages[i,]$dist,pages[i,]$src]<-1
A
}

#变换概率矩阵
probabilityMatrix<-function(G){
# 按列相加
cs <- colSums(G)
cs[cs==0] <- 1
n <- nrow(G)
A <- matrix(0,nrow(G),ncol(G))
for (i in 1:n) A[i,] <- A[i,] + G[i,]/cs
A
}

#递归计算矩阵特征值。跟权重有关的
eigenMatrix<-function(G,iter=100){
# iter<-10
n<-nrow(G)
x <- rep(1,n)
# 迭代,x为每一轮计算的权重,也是pR值。
for (i in 1:iter) x <- G %*% x
x/sum(x)
}

pages<-read.table(file="page.csv",header=FALSE,sep=",")
names(pages)<-c("src","dist");pages

以下便开始执行程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
> A<-adjacencyMatrix(pages);A

[,1] [,2] [,3] [,4]
[1,] 0 0 0 0
[2,] 1 0 0 1
[3,] 1 1 0 0
[4,] 1 1 1 0

> G<-probabilityMatrix(A);G

[,1] [,2] [,3] [,4]
[1,] 0.0000000 0.0 0 0
[2,] 0.3333333 0.0 0 1
[3,] 0.3333333 0.5 0 0
[4,] 0.3333333 0.5 1 0

> q<-eigenMatrix(G,10);q

[,1]
[1,] 0.0000000
[2,] 0.4036458
[3,] 0.1979167
[4,] 0.3984375

2)包括考虑阻尼系统的情况

dProbabilityMatrix是对于adjacencyMatrix函数的重构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#变换概率矩阵,考虑d的情况
dProbabilityMatrix<-function(G,d=0.85){
cs <- colSums(G)
cs[cs==0] <- 1
n <- nrow(G)
# (1-d)/n
delta <- (1-d)/n
A <- matrix(delta,nrow(G),ncol(G))
for (i in 1:n) A[i,] <- A[i,] + d*G[i,]/cs
A
}

> pages<-read.table(file="page.csv",header=FALSE,sep=",")
> names(pages)<-c("src","dist");pages

src dist
1 1 2
2 1 3
3 1 4
4 2 3
5 2 4
6 3 4
7 4 2

> A<-adjacencyMatrix(pages);A

[,1] [,2] [,3] [,4]
[1,] 0 0 0 0
[2,] 1 0 0 1
[3,] 1 1 0 0
[4,] 1 1 1 0

> G<-dProbabilityMatrix(A);G
[,1] [,2] [,3] [,4]
[1,] 0.0375000 0.0375 0.0375 0.0375
[2,] 0.3208333 0.0375 0.0375 0.8875
[3,] 0.3208333 0.4625 0.0375 0.0375
[4,] 0.3208333 0.4625 0.8875 0.0375

> q<-eigenMatrix(G,100);q
[,1]
[1,] 0.0375000
[2,] 0.3738930
[3,] 0.2063759
[4,] 0.3822311

增加阻尼系数后,ID=1的页面,就有值了PR(1)=(1-d)/n=(1-0.85)/4=0.0375,即无外链页面的最小值。

3)直接用R的特征值计算函数

增加的函数:calcEigenMatrix
用于直接计算矩阵特征值,可以有效地减少的循环的操作,提高程序运行效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#直接计算矩阵特征值.即特解
calcEigenMatrix<-function(G){
x <- Re(eigen(G)$vectors[,1])
x/sum(x)
}

> pages<-read.table(file="page.csv",header=FALSE,sep=",")
> names(pages)<-c("src","dist");pages
src dist
1 1 2
2 1 3
3 1 4
4 2 3
5 2 4
6 3 4
7 4 2

> A<-adjacencyMatrix(pages);A
[,1] [,2] [,3] [,4]
[1,] 0 0 0 0
[2,] 1 0 0 1
[3,] 1 1 0 0
[4,] 1 1 1 0

> G<-dProbabilityMatrix(A);G
[,1] [,2] [,3] [,4]
[1,] 0.0375000 0.0375 0.0375 0.0375
[2,] 0.3208333 0.0375 0.0375 0.8875
[3,] 0.3208333 0.4625 0.0375 0.0375
[4,] 0.3208333 0.4625 0.8875 0.0375

> q<-calcEigenMatrix(G);q
[1] 0.0375000 0.3732476 0.2067552 0.3824972

Gaussian Blur

前言

玩了OpenCV之后,感觉自己能分分钟写出个PS软件,真是嘚瑟了。不过,这倒是也是个方向。所以,现在就来写个滤镜吧。

说到底,在计算机眼中,所谓的图片就是一个矩阵,对于他的处理跟线性代数是极其相关的,所以大学狗还是认真学习去吧。好了,不闹了。说到滤镜,说到矩阵,当然要想到PS软件里面的”Gaussian Blur”啦。

目录

  1. Gaussian Blur的介绍

  2. Gaussian Blur的原理

  3. Gaussian Blur的例子以及代码实现

1. Gaussian Blur的介绍

高斯模糊(英语:Gaussian Blur),也叫高斯平滑,是在Adobe Photoshop、GIMP以及Paint.NET等图像处理软件中广泛使用的处理效果,通常用它来减少图像噪声以及降低细节层次。

这种模糊技术生成的图像,其视觉效果就像是经过一个半透明屏幕在观察图像,这与镜头焦外成像效果散景以及普通照明阴影中的效果都明显不同。高斯平滑也用于计算机视觉算法中的预先处理阶段,以增强图像在不同比例大小下的图像效果。 从数学的角度来看,图像的高斯模糊过程就是图像与正态分布做卷积。由于正态分布又叫作高斯分布,所以这项技术就叫作高斯模糊。图像与圆形方框模糊做卷积将会生成更加精确的焦外成像效果。由于高斯函数的傅立叶变换是另外一个高斯函数,所以高斯模糊对于图像来说就是一个低通滤波器。

本文介绍”高斯模糊”的算法,这是一个非常简单易懂的算法。本质上,它是一种数据平滑技术(data smoothing)

2. Gaussian Blur的原理

所谓”模糊”,可以理解成每一个像素都取周边像素的平均值。

是的,”平均值”是模糊的关键。他能够将清晰度高的照片,运用平均值,将图片的细节隐藏,使这个像素矩阵的变化更为平滑。

咱们先来看看个例子:

gs0

还未进行模糊处理过的图片,里面的像素数值是不定,细节暴露的。就比如上图,中间数值是2,周边是1。

gs1

“中间点”取”周围点”的平均值,就会变成1。在数值上,这是一种”平滑化”。在图形上,就相当于产生”模糊”效果,”中间点”失去细节。

显然,计算平均值时,取值范围越大,”模糊效果”越强烈。

gs2

上面分别是原图、模糊半径3像素、模糊半径10像素的效果。模糊半径越大,图像就越模糊。从数值角度看,就是数值越平滑。

这就是模糊的原理。而以上这个例子的模糊算法便是最常见的3x3模板的均值模糊,即取一定半径内的像素值之平均值作为当前点的新的像素值。

其实,这个时候,本文也算是结束了。因为,你会发现,其他图像模糊算法是大同小异的,只是在求均值的时候因为侧重点的不同而赋予这个矩阵的各个像素点以不同的权重而已。不过,我还是会继续讲下去的,不然会被打死。

那么我们接着来,大家一定听说过’Gaussian’这位伟大的学者吧。那么他有名的”高斯分布”,也就是”正态分布”也是知道的吧。对的,就是你想的那样。所谓的”高斯模糊”,就是将高斯分布模糊两者结合起来的,然后就随随便便的冠上了伟大的”Gsussian”的名头,让人还以为高大上,很难,其实就是求均值。

前面我们说到了”权重”,是想让这个矩阵中筛选出对于中间点是相对更加重要的点,并给它在求均值的时候更大的影响力。而高斯模糊中的权重函数,真是高斯分布。

gs3

由高斯分布可以知道,距离中点越近的点,显然他的权重是越大的;相反,越远,权重就越小。

计算平均值的时候,我们只需要将”中心点”作为原点,其他点按照其在正态曲线上的位置,分配权重,就可以得到一个加权平均值。

接下来祭出高斯分布的密度函数的计算公式:

GAOSI

其中,μ是x的均值,σ是x的方差。因为计算平均值的时候,中心点就是原点,所以μ等于0。

gaosi1

又因为像素矩阵是二维的,而现在这个密度函数是一维的。根据一维高斯函数,可以推导得到二维高斯函数:

gaosi2

3. Gaussian Blur的步骤以及代码实现

根据高斯模糊的原理,我们已经知道,要根据二维高斯函数求取像素矩阵的均值,以此来进行模糊处理。

1)求取权重矩阵

咱们还是使用的阮一峰老师博客的例子:

假定使用的3x3模板,中心点的坐标是(0,0),那么距离它最近的8个点的坐标如下,更远的点以此类推:

gs5

为了计算权重矩阵,需要设定σ的值。假定σ=1.5,分别将这个九宫格中的各个点的坐标代入上面的二维高斯函数计算公式,求出各自的G(x,y).则模糊半径为1的权重矩阵如下:

gs6

这9个点的权重总和等于0.4787147,如果只计算这9个点的加权平均,还必须让它们的权重之和等于1,因此上面9个值还要分别除以0.4787147,得到最终的权重矩阵。

gs7

2)计算高斯模糊

就是说计算中间点的值。

假设现有9个像素点,灰度值(0-255)如下:

gs8

各自乘上各自对应的权重。

gs9

gs10

将这九个值相加即可得到中间点的数值。这就是中间点高斯模糊值。

对所有点重复这个过程,就得到了高斯模糊后的图像。如果原图是彩色图片,可以对RGB三个通道分别做高斯模糊。

对于边缘的点,就是把已有的点拷贝到另一面的对应位置,模拟出完整的矩阵。

那么接下是代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92

import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;

public class GaussianBlur {

/**
 * 简单高斯模糊算法
 * @param args
 * @throws IOException [参数说明]
 * @return void [返回类型说明]
 * @exception throws [违例类型] [违例说明]
 * @see [类、类#方法、类#成员]
 */
public static void main(String [] args) throws IOException {
BufferedImage img = ImageIO.read(new File("BigData.jpg"));
System.out.println(img);
int height = img.getHeight();
int width = img.getWidth();

/**
* 计算一个九宫格的平均值
*/
int[][] matrix = new int[3][3];
int[] values = new int[9];
for (int i = 0;i<width;i++){
for(int j = 0;j<height;j++){
readPixel(img,i,j,values);
fillMatrix(matrix,values);
img.setRGB(i, j, avgMatrix(matrix));
}
}
ImageIO.write(img,"jpeg",new File("test.jpg"));//保存在工程为test.jpeg文件
}

private static void readPixel(BufferedImage img, int x, int y, int[] pixels){
int xStart = x - 1;
int yStart = y - 1;
int current = 0;
//
for (int i=xStart;i<3+xStart;i++){
for(int j=yStart;j<3+yStart;j++){
int tx=i;
if(tx<0){
tx=-tx;
}else if(tx>=img.getWidth()){
tx=x;
}

int ty=j;
if(ty<0){
ty=-ty;
}else if(ty>=img.getHeight()){
ty=y;
}
pixels[current++]=img.getRGB(tx,ty);
}
}
}

private static void fillMatrix(int[][] matrix, int... values) {
int filled=0;
for(int i=0;i<matrix.length;i++){
int[] x=matrix[i];
for(int j=0;j<x.length;j++){
x[j]=values[filled++];
}
}
}

private static int avgMatrix(int[][] matrix){
int r=0;
int g=0;
int b=0;
for(int i=0;i<matrix.length;i++){
int[] x=matrix[i];
for(int j=0;j<x.length;j++){
if(j==1){
continue;
}
Color c=new Color(x[j]);
r+=c.getRed();
g+=c.getGreen();
b+=c.getBlue();
}
}
return new Color(r/8,g/8,b/8).getRGB();
}
}

再来个wiki上面的例子吧,大家可以巩固一下:

GSexample

Service on Android

前言

Service是Android组件中最基本也是最为常见用的四大组件之一。

service可以在和多场合的应用中使用,比如播放多媒体的时候用户启动了其他Activity这个时候程序要在后台继续播放,比如检测SD卡上文件的变化,再或者在后台记录你地理信息位置的改变等等,总之服务嘛,总是藏在后头的。

Service是在一段不定的时间运行在后台,不和用户交互应用组件。每个Service必须在manifest中通过来声明。可以通过contect.startservice和contect.bindserverice来启动。

Service和其他的应用组件一样,运行在进程的主线程中。这就是说如果service需要很多耗时或者阻塞的操作,需要在其子线程中实现。

目录

  1. Service的生命周期及启动模式
  2. Service的分类
  3. Service与Thread的区别
  4. Service的优先级

一、Service的生命周期及启动模式

ServiceLoop.png

1、使用context.startService() 启动Service是会会经历:
context.startService() ->onCreate()- >onStart()->Service running
context.stopService() ->onDestroy() ->Service stop

startService的时候,如果Service还没有运行,则android先调用onCreate()然后调用onStart();如果Service已经运行,则只调用onStart(),所以一个Service的onStart方法可能会重复调用多次。

stopService的时候,直接onDestroy,如果是调用者自己直接退出而没有调用stopService的话,Service会一直在后台运行。该Service的调用者再启动起来后可以通过stopService关闭Service。

2、使用使用context.bindService()启动Service会经历:
context.bindService()->onCreate()->onBind()->Service running
onUnbind() -> onDestroy() ->Service stop

onBind将返回给客户端一个IBind接口实例,IBind允许客户端回调服务的方法,比如得到Service运行的状态或其他操作。这个时候把调用者(Context,例如Activity)会和Service绑定在一起,Context退出了,Srevice就会调用onUnbind->onDestroy相应退出。

所以调用bindService的生命周期为:onCreate –> onBind(只一次,不可多次绑定) –> onUnbind –> onDestory。

在Service每一次的开启关闭过程中,只有onStart可被多次调用(通过多次startService调用),其他onCreate,onBind,onUnbind,onDestory在一个生命周期中只能被调用一次。

3、被启动startService又被绑定bindService的服务的生命周期:如果一个Service又被启动又被绑定,则该Service将会一直在后台运行。并且不管如何调用,onCreate始终只会调用一次,对应startService调用多少次,Service的onStart便会调用多少次。

此时,Service 的终止,需要unbindService与stopService同时调用,才能终止 Service,不管 startService 与 bindService 的调用顺序,如果先调用 unbindService 此时服务不会自动终止,再调用 stopService 之后服务才会停止,如果先调用 stopService 此时服务也不会终止,而再调用 unbindService 或者 之前调用 bindService 的 Context 不存在了(如Activity 被 finish 的时候)之后服务才会自动停止;

4、启动service,根据onStartCommand的返回值不同,有两个附加的模式:
1)START_STICKY 用于显示启动和停止service。
2)START_NOT_STICKY或START_REDELIVER_INTENT用于有命令需要处理时才运行的模式。

startService与bindService的区别:
1、使用startService()方法启用服务,调用者与服务之间没有关连,即使调用者退出了,服务仍然运行。
如果打算采用Context.startService()方法启动服务,在服务未被创建时,系统会先调用服务的onCreate()方法,接着调用onStart()方法。

如果调用startService()方法前服务已经被创建,多次调用startService()方法并不会导致多次创建服务,但会导致多次调用onStart()方法。

采用startService()方法启动的服务,只能调用Context.stopService()方法结束服务,服务结束时会调用onDestroy()方法。

2、使用bindService()方法启用服务,调用者与服务绑定在了一起,调用者一旦退出,服务也就终止,大有“不求同时生,必须同时死”的特点。
onBind()只有采用Context.bindService()方法启动服务时才会回调该方法。该方法在调用者与服务绑定时被调用,当调用者与服务已经绑定,多次调用Context.bindService()方法并不会导致该方法被多次调用。

采用Context.bindService()方法启动服务时只能调用onUnbind()方法解除调用者与服务解除,服务结束时会调用onDestroy()方法。

在什么情况下使用 startService 或 bindService 或 同时使用startService 和 bindService?

1、如果你只是想要启动一个后台服务长期进行某项任务那么使用 startService 便可以了。

2、如果你想要与正在运行的 Service 取得联系,那么有两种方法,一种是使用 broadcast ,另外是使用 bindService .

1)broadcast 的缺点是如果交流较为频繁,容易造成性能上的问题,并且 BroadcastReceiver 本身执行代码的时间是很短的(也许执行到一半,后面的代码便不会执行)

2)bindService 则没有这些问题,因此我们肯定选择使用 bindService(这个时候你便同时在使用 startService 和 bindService 了,这在 Activity 中更新 Service 的某些运行状态是相当有用的)。

3、如果你的服务只是公开一个远程接口,供连接上的客服端(android 的 Service 是C/S架构)远程调用执行方法。这个时候你可以不让服务一开始就运行,而只用 bindService ,这样在第一次 bindService 的时候才会创建服务的实例运行它,这会节约很多系统资源,特别是如果你的服务是Remote Service,那么该效果会越明显(当然在 Service 创建的时候会花去一定时间,你应当注意到这点)。

另外注意的是,当在旋转手机屏幕的时候,当手机屏幕在“横”“竖”变换时,此时如果你的 Activity 如果会自动旋转的话,旋转其实是 Activity 的重新创建,因此旋转之前的使用 bindService 建立的连接便会断开(Context 不存在了),对应服务的生命周期与上述相同。

二、Service的分类

按运行地点分类:

ServiceL.png

其实remote服务还是很少见的,并且一般都是系统服务。

按运行类型分类:

ServiceQ.png

有同学可能会问,后台服务我们可以自己创建 ONGOING 的 Notification 这样就成为前台服务吗?答案是否定的,前台服务是在做了上述工作之后需要调用 startForeground ( android 2.0 及其以后版本 )或 setForeground (android 2.0 以前的版本)使服务成为 前台服务。这样做的好处在于,当服务被外部强制终止掉的时候,ONGOING 的 Notification 任然会移除掉。

按使用方式分类:

ServiceW.png

三、Service与Thread的区别

很多时候,你可能会问,为什么要用 Service,而不用 Thread 呢,因为用 Thread 是很方便的,比起 Service 也方便多了,下面来解释一下。

1、Thread:Thread 是程序执行的最小单元,它是分配CPU的基本单位。可以用 Thread 来执行一些异步的操作。

2、Service:Service 是android的一种机制,当它运行的时候如果是Local Service,那么对应的 Service 是运行在主进程的 main 线程上的。如:onCreate,onStart 这些函数在被系统调用的时候都是在主进程的 main 线程上运行的。如果是Remote Service,那么对应的 Service 则是运行在独立进程的 main 线程上。因此请不要把 Service 理解成线程,它跟线程半毛钱的关系都没有!

既然这样,那么为什么要用 Service 呢?

其实这跟 android 的系统机制有关,我们先拿 Thread 来说。Thread 的运行是独立于 Activity 的,也就是说当一个 Activity 被 finish 之后,如果你没有主动停止 Thread 或者 Thread 里的run 方法没有执行完毕的话,Thread 也会一直执行。因此这里会出现一个问题:当 Activity 被 finish 之后,你不再持有该 Thread 的引用。另一方面,你没有办法在不同的 Activity 中对同一Thread 进行控制。

举个例子:如果你的 Thread 需要不停地隔一段时间就要连接服务器做某种同步的话,该 Thread 需要在 Activity 没有start的时候也在运行。这个时候当你 start 一个 Activity 就没有办法在该 Activity 里面控制之前创建的 Thread。因此你便需要创建并启动一个 Service ,在 Service 里面创建、运行并控制该 Thread,这样便解决了该问题(因为任何 Activity 都可以控制同一 Service,而系统也只会创建一个对应 Service 的实例)。

因此你可以把 Service 想象成一种消息服务,而你可以在任何有 Context 的地方调用 Context.startService、Context.stopService、Context.bindService,Context.unbindService,来控制它。你也可以在 Service 里注册 BroadcastReceiver,在其他地方通过发送 broadcast 来控制它,当然这些都是 Thread 做不到的。

四、Service的优先级

拥有service的进程具有较高的优先级。只要在该service已经被启动(start)或者客户端连接(bindService)到它,Android系统会尽量保持拥有service的进程运行。当内存不足时,需要保持拥有service的进程。

1、如果service正在调用onCreate,onStartCommand或者onDestory方法,那么用于当前service的进程则变为前台进程以避免被killed。

2、如果当前service已经被启动(start),拥有它的进程则比那些用户可见的进程优先级低一些,但是比那些不可见的进程更重要,这就意味着service一般不会被killed.

3、如果客户端已经连接到service(bindService),那么拥有Service的进程则拥有最高的优先级,可以认为service是可见的。

4、如果service可以使用startForeground(int, Notification)方法来将service设置为前台状态,那么系统就认为是对用户可见的,不会在内存不足时killed。

5、如果有其他的应用组件作为Service,Activity等运行在相同的进程中,那么将会增加该进程的重要性。

不只是三面

前天,5月6号收到腾讯offer通知的时候,感到终于松了一口气。为了这个offer我也是跑了不少地方了,也可以很清晰的看得到这一两年来的变化。两个月的风尘,也可以暂时停下了。

回顾这次的能够得到腾讯的offer,我只有一句话可以总结:

这个offer,不只是只有三面。

缘由

关于腾讯,我是面过了好多次了,不管是内推,还是说正常流程,都有参与过,但是那时候还是初出茅庐,对于面试还是不得要领,一直弄得很是不顺。心中虽有不甘,但是,却也没有想过说放弃任何一次机会。尤其是大三下了,虽说已经有了offer在身,却还是想要去尝试一番。

于是,当森然给我提及到,这次腾讯在找长沙站实习生招聘的志愿者的时候,我立马给腾讯这次长沙站的HR发了个邮件自荐。恰好,仿佛是命运眷顾,这位HR正是,我去年大二的时候,也是差不多这个时候腾讯霸面时候认识的中南的学长,那时候他是刚进入腾讯帮忙招聘。那时候虽然霸面没成功(其实霸面确实比正常流程的难,不过不失是一次机会),但是最大的收获是认识了学长满哥。就这样,我成了这次长沙站的志愿者。

而后,便又给学长投了一份简历。

真的很感谢满哥,真的很热心,因为不知道是什么原因,我的简历被卡在了内推二面环节,当时的面试官没有释放我的简历,导致我无法进行正常流程,霸面也不行,而感谢满哥能够不厌其烦的为我跟那个面试官沟通,释放简历,给了我一次、也是实习生招聘的最后一次面试机会。很是宝贵。

初试

因为我是志愿者,在长沙站的这一周的招聘里面,我是有工作需要做的,没有多少时间准备。不过庆幸的是,以前的面试经验给了我很大的帮助,而且在工作之余,我热心的跟来面试来霸面的同学交流搭讪,能够感觉到大家都是很厉害的人,也能够更加体会到,作为面试官,作为一家公司,他们需要的会是一位怎样的员工。

那是第一天招聘的下午,轮到我去面试了。换班、上楼。很奇怪,没有很紧张,估计是在此之前了解的比较清楚了,舌头也说开了吧。具体面试的问题不是太记得清楚了。但是记得面试官问的大多是我的项目的问题,自己也对项目做足了功课的,所以没有太难。不过,面的时间有点短,感觉没有尽情的展示了自己的清楚,所以对结果有点紧张。
之后在志愿者工作的时候,我又几次遇到初试官下来拿简历或是拿饭,很开心,便跟他搭上话来。其实面试官真的很好说话的。

当天晚上,没有收到通知,状态也没有变化。

次日下午,再次刷微信查询的时候,终于状态变成了复试,自己一个人在走道上跟疯了似的,又蹦又跳的。

interview2.png

复试

收到复试通知后,当晚,我开始有点像高考复习一样,复习我所知道的知识点,尤其是数据结构中的树、图,算法导论中的排序、查找、分治、动态规划等等,博客(写博客真的是很重要,不论是在复习的时候,还是在面试的时候,因为面试的时候你可以直接展示给面试官你的成果,当然,你也要熟悉才行,最好是原创的),JVM等等。

我有做笔记的习惯,在以前的面试的时候,我还专门列了下自己所能够展示的知识点、加分项。所以,这次的复试准备的算是很充足了。

但是,剧情确实曲折的,我没想到的是,在一开始复试官便给了我一道完全没有思考过的问题(也是自己作死,在自我介绍的时候给自己挖了个坑):C原因中,相同的Int,一个赋值为0,一个赋值为500,这两个Int有什么区别。这个真是没有回答的很好,但是,我说了下自己的理解。

之后,给了我一道题,手写代码。这道题的思路是很简单的一道需要排序的二分查找。虽然简单,但花了点时间,想用归并排序,出了问题。这也是我这次面试觉得最大的遗憾的地方。

这个时候了,我是有点方了。面试官人很好,也没有问多少我的技术方卖弄的问题了。开始问我,为什么选择腾讯,为什么想要做这个岗位,有什么想要问他的这样的问题。

虽然已经知道这位复试官面试的时候大多在20分钟左右,但没想到这么快就抛出了这个问题,我心想这是要悬。所以,在回答之后的这些问题的时候,我努力表现自己的诚意,也表现出自己在腾讯之后的职业发展,自己的目标明确,这几年的经历以及努力。最后又表示自己对这次面试的态度,说,自己对这次面试是不满意的,不管是第一个问题,还是拿到手写代码,都不是自己满意的,说自己一直在写《剑指offer》的试题,并上传到了自己的github上面了,也在经营分享自己的微信公众号。

复试官显然是感受到了我的执着。于是看了下我的初试官的评语,提了到我的初试官也问过的问题,也是我写过的一篇博客探讨过的问题——Android 的热修复方案。一道题,仿佛找到突破口一般,我开始沿着我的思路,把Android热修复的四大方案讲完,有发散到JVM以及GC算法上面了,思路清晰,有条有理。

还没等讲完,复试官便直接给了我通过的回复。真的,我整个人是飘着下来的,一直怀疑自己是不是听错了。因为我能够感觉到自己这么久来的努力在被肯定了。一扫前几天来的不顺。

当晚,我的状态变成了HR面。

interviewHR.png

HR面

虽说,HR面是不怎么刷人了,但是鉴于武汉站的时候,那么多的HR面被刷的前车之鉴,我还是google了下,仔细准备HR面。其实也不是很知道如何准备,主要是需要放松心态,把HR当做是同事朋友,大家坐在一起聊一聊自己的经历看法见解。

这次是涛哥面我,因为之前在志愿者工作的时候,便与涛哥坐在一起吃过饭,了解到涛哥是一个很会聊天的人,所以,不是很紧张。跟涛哥聊得很开心,而且之后我发现,我的面试估计都可以称之为典范了吧,因为所有要问的问题我都被问了,其他人没有被问的我也被问了。

这次HR面,我可以说也是知道是通过了的。

所以,当次日刷状态的时候,我的状态改变了。

interviewAll.png

但是这时,你还不能说稳了。因为这只是说你的面试过了而已,之后,关于通过者的信息会传回到腾讯总部你将要在的部门(大多是你复试官所在的部门),让部门经理审核。所以,还不能说稳了,因为之后没能得到offer的人也不是没有。

一周之后,我接到了腾讯打来的电话,确认offer,签约。

关于简历

简历真是敲门砖,是HR认识到你的第一印象。所以,简历一定要排版好,该写的写上,不会的、不熟悉的、不是十分有分量的不要写。然后简历打印不要用硬纸板打印,使用普通的纸打印就行了。
而面产品的,我的建议是,附上的材料不要太多,捡重要的附上,简洁明了,表达你自己的见解就行了。当然,材料在排版、色调方面最好也要注意。

关于霸面

1、最好是停留在现场等待。虽说,腾讯招聘的时候,有专门的霸面区接收霸面简历,HR也让霸面的同学回去等待。但是综合我的所见,我发现最好还是停留在现场等待。因为HR哥哥姐姐也是过来人,都知道霸面是需要更多的勇气的,也理解霸面同学的焦急无奈的情绪,他们都是很好的人,也会想办法给你机会的。再者是,面试官面试速度是不一样的,一天的面试人数也是不一样的,有时面试官会下来拿霸面同学的简历,而在现场的同学更加有机会与面试官攀谈上,率先让他过目简历。

2、要灵活。关于霸面,也是需要动动脑筋的。不是说你在那儿等就行了,总会有机会的,机会也是需要争取的,运气也是需要脱颖而出的。相比于在现场傻傻等待面试机会的同学,我更加佩服的是那些不只是把眼光放于当前这场招聘的同学。一、你需要等待表现出诚意,也要表现在HR面前,让他们看到你的努力,最好是有机会能够与HR攀谈上,表现出你自己,与他们成为朋友,这是最好的结果,再不济也能够给他们留个眼熟啊。二、如果你不好意思与HR交流,毕竟还是有点尴尬的,你也可以与在场的面试的同学交流,了解了解你的对手都是一些什么样的人,大家对于面试的想法,面试官都面了什么,他们是怎么回答的,你又该怎么回答,自己有什么不足等等。而且面试现场真的也有好多的美女咧,交几个朋友总不是什么坏事。

3、当你觉得你已经足够勇敢的时候,其实你还需要更加的勇敢。这一点真是在霸面产品的同学身上体现的十分贴切啊。知道,面试现场的好多人都是专门从其他城市赶过来的,比如武汉、南昌、南宁,甚至是北京,很多的研究生,很多的在其他站HR面被拒的想要再抓住机会的同学,大家都是十分的努力,也是足够的勇敢的。但有时候,结果不是单单的努力就会有结果的,运气也不会一直眷顾你。这个时候,你就需要比那些好运的实力更强的又足够勇敢的变得更加勇敢才行。这次长沙站守电梯,送面试者上去面试的人是我。很多人跑来跟我说,帮忙刷他们上去,给他们一个机会。因为职责所在,没有答应。但是人不能够死脑筋,总会有办法的。很多面产品的同学,还是找到了其他办法,没有经过我,刷上去了面试官的楼层。虽然结果不是很理想,但是这是勇敢给他们制造的又多的一个机会。当然,我不是很推荐这种做法就是了,但是也是看不同面试官的。

4、凡事有个度。霸面是一种勇气的表现,但是需要一个度。也是这次长沙站,有个同学就多次想办法上去直接找面试官。我不知道结果如何,但是如果我是面试官,我也会觉得有点厌烦的。因为招聘就像相亲,看顺眼了,适合了自然而然就会成,自然而然就会过,不必太过于纠缠。还有就是,在霸面的过程中要有礼貌,遵守流程,不要无理取闹,听取工作人员的建议以及警惕工作人员的警告。腾讯是有一个黑名单的,无视警告的都会被计入黑名单之中。

关于面试

面经永远是别人的面经,最好的办法是自己亲临战场去经历去成长。在面试的时候,展示你的所能,坦然真诚坚持富有激情拥有潜力。

后记

最后,真的是感谢森然,感谢满哥,也感谢一起志愿服务的朋友们,也感谢自己的坚持。面试了这么多次,这次是第一次走完整个流程的,以前也没有,以前得到的offer也都是直接跟老大聊聊就成了的。整个流程下来,真的成长了不少,也涨了见识。这次是我距离腾讯offer最近的一次,还好我没有辜负满哥的期待。

谨以此文,记录此刻。

About Hadoop

前言

我们知道,互联网近十年的数据增长是呈现爆炸增长的。大型互联网公司每天要处理的数据基本上都是TB级,甚至是PB级的。而为了存储处理这样海量的数据,传统的数据存储策略(关系型数据库)便陷入了瓶颈。不管是数据入库还是查询,使用传统的存储策略都设计到上亿条数据的便利查找操作,显然这使相当考验存储设备的性能的,也是相当耗时的。但我们知道,当代互联网是追求实时性、高用户体验的,这样的等待时间显然是不会被 认可的。

另外,在追求计算机性能的解决方案时,也必然意味着这样的计算机事极其昂贵、无法普及的,就像现在的超算中心一样,只能 是属于少数人的,大规模组织的。不利于小公司的发展,同样也不适应当代互联网的发展趋势。

而且,相比于互联网海量数据的增长速度,这样的一台高性能的机器,是否过不久还适合处理这样不断增长数据,还是一个问题呢。

于是,为适应这样的状况,有人就开始设计出了分布式系统,于是,Hadoop就出现了。

目录

1.Hadoop的介绍

2.Hadoop的两大组件

3.Hadoop的实现机制

1.Hadoop的介绍

Hadoop是一个分布式计算开源 软件框架。能在有大量廉价的硬件设备组成的集群上运行应用程序,存储处理海量级别数据。

Hadoop的核心组件有两个,分别是分布式存储、分布式文件管理系统HDFS(Hadoop Distributed File System),还有就是分布式计算框架MapReduce。

其实,Hadoop并不是一个原创的软件。因为在Hadoop之前已经有了很多的分布式框架,比如最著名的是Google的GFS、BigTable、MapReduce,Amason的 AWS、微软的Azure和IBM的蓝云。但是这些软件都是闭源的、商业性的。而这时,Hadoop的创建者Doug Cutting就出现了,应Yahoo!的邀请,他开始在Yahoo!带领团队,开发Apache基金会的顶尖项目Hadoop。应该所Hadoop就是个山寨货,他就是Doug Cutting根据Google的三大论文的思想开发出来的,那个分布式计算框架MapReduce与Google的一模一样,连名字都不带改的。但是这并不妨碍Hadoop成为现如今最有影响力的软件。毕竟,这就是开源的力量。

2.Hadoop的两大组件

2.1 HDFS

  • Namenode:名称节点,hdfs的管理程序。控制存储和IO交换。保存元数据。

  • Secondary NameNode :辅助名称节点。与NomeNode通讯,复制并合并元数据(比如文件命名空间镜像、修改日志),合并为一个文件,并检查文件大小,将就得修改存入本地磁盘,防止文件过大。更新到NameNode之中。这个过程可以简称为“快照”。当NameNode崩溃是,可手动切换成NameNode,代替NameNode完成工作。

  • dataNode:是数据存储节点。

至于HDFS上面的任务管理器,旧版本的有JobTracker、TaskTracker,而2.x版本之后,为了更便于资源管理,Hadoop便使用ResourceManager替代以上组件。但这里还是以旧版本为例吧。

  • JobTracker:运工作监控器。行在主服务器Master节点里面,就近运行。操作切割分配监控task。

以上便是Hadoop的基本存储架构。

HDFS典型的部署是在一个专门的机器 运行NameNode,集群中的其他机器各运行一个DataNode,当然,也可以在运行NameNode的机器上面运行DataNode,或者一个机器运行多个DataNode。一个集群只能有一个NameNode。

NameNode使用事务日志(EditLog)来记录HDFS元数据的变化,使用映射文件(FSImage)存储文件系统的命名空间,包含文件的映射、文件的属性信息等。事务日志和映射文件都存储在NameNode的本地文件系统之中。NameNode启动时,从磁盘中读取映射文件和事务日志,把事务日志的事务都应用到内存中的映射文件上,然后将新的元数据刷新到本地磁盘的新的映射文件上,这样可以截去旧的事务日志,这个过程称为检查点(CheckPoint)。HDFS还设有Secondary NameNode节点,他辅助NameNode处理映射文件和事务日志。NameNode启动的时候合并映射文件和事务日志,而Secondary NameNode周期性的从NameNode复制映射和事务日志到临时目录里面,合并成新的映射文件后再重新上传给NameNode,NameNode更新映射文件并处理事务日志,使得事务日志的大小始终控制在可配置的限度之下。

架构

架构

2.2 MapReduce

M/R就是Hadoop底下的并行计算模型。顾名思义,他就是有Map还有Reduce两个部分组成的 。Map是把一组数据一对一的映射成为另外的一组数据,Reduce是对一组数据进行归约,映射和归约分别有一个函数指定。具体的就交由大家在具体项目开发的时候在体会了,这样效果更佳哦。

3.Hadoop的实现机制

1.机柜意识

通常,大型的HDFS集群跨多个安装点(机柜)排列。一个安装点中的不同节点之间的网络流量通常比跨安装点的网络流量更加的高校。一个NameNode尽量将一个块的多个副本放在多个安装点上以提高容错能力。但是,要平衡容错能力与网络流量的话,HDFS就允许管理员决定一个节点属于哪个安装点,于是,每一个节点就都可以知道它的机柜ID,也就是说,具有了机柜意识。

NameNode可以通过DataNode的机柜ID识别它们的位置。

2.冗余备份

HDFS是一个大集群、跨机器、可靠存储的大型文件系统。每个文件都存储在一个个的数据块之中,默认数据块大小为64MB。同一个文件中除了最后一块以外的所有都是一样大的。文件的数据块通过复制备份来保证容错。当然,文件的数据块的大小和复制因子都是可以进行配置的。

HDFS的文件都是一次性写入的,并且都是严格限制任何时候都是一个写用户。DataNode使用本地文件系统来存储HDFS的数据,但是它对于HDFS的文件是一无所知的,只是使用一个个文件来存储HDFS的数据块。每当DataNode启动的时候,都会先进行一次对于本地文件系统的便利,得到一份关于HDFS数据块与本地额对应关系列表,并把这个列表报告发给NameNode。这表示块报告,块报告包含了DataNode上所有块的列表。

3.副本存放

HDFS集群一般运行在多个机架之上,不同的机架上机器的通信是通过交换机的。通常,机架内节点之间的带宽比跨机架节点之间的带宽大、更快,而这会影响HDFS的可靠性以及性能。而HDFS会通过机架感知(Rack-aware,NameNode可以确定每个机架的所属的ID)来改进平衡数据可靠性。

一般,当复制因子是3的时候,HDFS的数据块备份策略是,将两个副本放在同一个机架的不同节点上,最后一个副本放在另外的一个机架的节点上。这样就可以防止整个机架失效的时候数据丢失。

4.HDFS心跳

因为在这样的大型集群里面,宕机是一种常态。所以,NameNode与DataNode之间的连通性,随时有可能会失效。因此,每个DataNode都会主动向NameNode发送定期心跳信息,如果NameNode没有接收到对应的DataNode的心跳信息,就表明连通性丧失。然后,NameNode会将不能响应心跳信息的DataNode标记为“死DataNode”,并不再向他们发送请求,而存储在该DataNode节点上的数据将不再对那个节点的HDFS客户端可用,该节点将被从系统中有效的移除。如果一个DataNode的死导致数据库的复制因子降到最小值以下,NameNode将会启动附加复制,将复制因子带回正常阶段。

5.安全模式

HDFS启动是时,NameNode会进入一个特殊的模式,安全模式,此时不会出现数据块的复制。NameNode会受到个DataNode的心跳信息以及数据块报告。数据块报告中包含一个DataNode所拥有的数据块列表,每个数据块里面都有指定书目的副本。当某个NameNode登记的数据复制品达到最小数目(<=最小数目)时,数据块会被认为是安全的,安全复制。在一定百分比(可配置)的数据块被NameNode检测确定是安全登记之后,在登记(加上附加的30秒),NameNode会推出安全模式。当检测到副本数不足的数据块时,该数据块会被复制到其他的节点之上,以达到最小副本数。

6.数据完成性检测

从DataNode中获取的数据块可能会是损坏的。为此,HDSF客户端实现了对于HDFS文件内容的校验和检查(Checksum)。在HDFS文件被创建时,会计算每个数据块的检验和,并将检验和作为一个单独的隐藏文件保存在命名空间里面。当获取这个文件后,会检查各个DataNode取出的数据块和相应的校验和进行匹配。如若不匹配,则会选择其他副本的DataNode去数据块。

7.空间回收

当文件被删除是,文件会被移动到/trash目录下,只要还在这个目录之下,文件就可以被快速回复。文件在这个目录里面的时间是可以设置的,超过这个时间,系统就会把它从系统的命名空间之中删除。而文件的删除便会引起相应数据块的释放。

8.元数据磁盘失效

映射文件和事务日志是HDFS的核心数据结构。若果这些文件损坏,将会导致HDFS的不可用。当然,可以设置NameNode支持维护映射文件和事务日志的多个副本,任何映射文件或事务日志的修改都会同步到他的副本上。每次NameNode重启时,都会选择最新的一致性的映射文件和事务日志。

9.快照

快照支持存储某一个时间点的数据复制。利用这一特性可以将损坏的HDFS回滚到以前的某一个正常的时间点。

以上便是最近的一些心得,还请斧正。

You need to set client_id and slot_id to show this AD unit. Please set it in _config.yml.