Java RandomAccessFile用法

RandomAccessFile RandomAccessFile是用来访问那些保存数据记录的文件的,你就可以用seek()方法来访问记录,并进行读写了。这些记录的大小不必相同;但是其大小和位置必须是可知的。但是该类仅限于操作文件。

RandomAccessFile不属于InputStream和OutputStream类系的。实际上,除了实现DataInput和DataOutput接口之外 (DataInputStream和DataOutputStream也实现了这两个接口),它和这两个类系毫不相干,甚至不使用InputStream和OutputStream类中已经存在的任何功能;它是一个完全独立的类,所有方法 (绝大多数都只属于它自己)都是从零开始写的。这可能是因为RandomAccessFile能在文件里面前后移动,所以它的行为与其它的I / O类有些根本性的不同。总而言之,它是一个直接继承Object的,独立的类。

基本上,RandomAccessFile的工作方式是,把DataInputStream和DataOutputStream结合起来,再加上它自己的一些方法,比如定位用的getFilePointer(),在文件里移动用的seek(),以及判断文件大小的length()、skipBytes()跳过多少字节数。此外,它的构造函数还要一个表示以只读方式 ("r"),还是以读写方式 ("rw")打开文件的参数 (和C的fopen()一模一样)。它不支持只写文件。

只有RandomAccessFile才有seek搜寻方法,而这个方法也只适用于文件。BufferedInputStream有一个mark()方法,你可以用它来设定标记 (把结果保存在一个内部变量里),然后再调用reset()返回这个位置,但是它的功能太弱了,而且也不怎么实用。

RandomAccessFile的绝大多数功能,但不是全部,已经被JDK 1.4的nio的"内存映射文件(memory-mapped files)"给取代了,你该考虑一下是不是用"内存映射文件"来代替RandomAccessFile了。

import java.io.IOException;
import java.io.RandomAccessFile;
public class TestRandomAccessFile {
    public static void main(String[] args) throws IOException {
        RandomAccessFile rf = new RandomAccessFile("rtest.dat", "rw");
        for (int i = 0; i < 10; i++) {
            //写入基本类型double数据  
            rf.writeDouble(i * 1.414);
        }
        rf.close();
        rf = new RandomAccessFile("rtest.dat", "rw");
        //直接将文件指针移到第5个double数据后面  
        rf.seek(5 * 8);
        //覆盖第6个double数据  
        rf.writeDouble(47.0001);
        rf.close();
        rf = new RandomAccessFile("rtest.dat", "r");
        for (int i = 0; i < 10; i++) {
            System.out.println("Value " + i + ": " + rf.readDouble());
        }
        rf.close();
    }
}

内存映射文件内存映射文件能让你创建和修改那些因为太大而无法放入内存的文件。有了内存映射文件,你就可以认为文件已经全部读进了内存,然后把它当成一个非常大的数组来访问。这种解决办法能大大简化修改文件的代码。fileChannel.map(FileChannel.MapMode mode, long position, long size)将此通道的文件区域直接映射到内存中。注意,你必须指明,它是从文件的哪个位置开始映射的,映射的范围又有多大;也就是说,它还可以映射一个大文件的某个小片断。

MappedByteBuffer是ByteBuffer的子类,因此它具备了ByteBuffer的所有方法,但新添了force()将缓冲区的内容强制刷新到存储设备中去、load()将存储设备中的数据加载到内存中、isLoaded()位置内存中的数据是否与存储设置上同步。这里只简单地演示了一下put()和get()方法,除此之外,你还可以使用asCharBuffer()之类的方法得到相应基本类型数据的缓冲视图后,可以方便的读写基本类型数据。

import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
public class LargeMappedFiles {
    static int length = 0x8000000; // 128 Mb  
    public static void main(String[] args) throws Exception {
        // 为了以可读可写的方式打开文件,这里使用RandomAccessFile来创建文件。  
        FileChannel fc = new RandomAccessFile("test.dat", "rw").getChannel();
        //注意,文件通道的可读可写要建立在文件流本身可读写的基础之上  
        MappedByteBuffer out = fc.map(FileChannel.MapMode.READ_WRITE, 0, length);
        //写128M的内容  
        for (int i = 0; i < length; i++) {
            out.put((byte)'x');
        }
        System.out.println("Finished writing");
        //读取文件中间6个字节内容  
        for (int i = length / 2; i < length / 2 + 6; i++) {
            System.out.print((char) out.get(i));
        }
        fc.close();
    }
}

尽管映射写似乎要用到FileOutputStream,但是映射文件中的所有输出必须使用RandomAccessFile,但如果只需要读时可以使用FileInputStream,写映射文件时一定要使用随机访问文件,可能写时要读的原因吧。

该程序创建了一个128Mb的文件,如果一次性读到内存可能导致内存溢出,但这里访问好像只是一瞬间的事,这是因为,真正调入内存的只是其中的一小部分,其余部分则被放在交换文件上。这样你就可以很方便地修改超大型的文件了 (最大可以到2 GB)。注意,Java是调用操作系统的"文件映射机制"来提升性能的。

RandomAccessFile类的应用:

/* 
 * 程序功能:演示了RandomAccessFile类的操作,同时实现了一个文件复制操作。 
 */
package com.lwj.demo;
import java.io. * ;
public class RandomAccessFileDemo {
    public static void main(String[] args) throws Exception {
        RandomAccessFile file = new RandomAccessFile("file", "rw");
        // 以下向file文件中写数据  
        file.writeInt(20); // 占4个字节  
        file.writeDouble(8.236598); // 占8个字节  
        file.writeUTF("这是一个UTF字符串"); // 这个长度写在当前文件指针的前两个字节处,可用readShort()读取  
        file.writeBoolean(true); // 占1个字节  
        file.writeShort(395); // 占2个字节  
        file.writeLong(2325451l); // 占8个字节  
        file.writeUTF("又是一个UTF字符串");
        file.writeFloat(35.5f); // 占4个字节  
        file.writeChar('a'); // 占2个字节  
        file.seek(0); // 把文件指针位置设置到文件起始处  
        // 以下从file文件中读数据,要注意文件指针的位置  
        System.out.println("——————从file文件指定位置读数据——————");
        System.out.println(file.readInt());
        System.out.println(file.readDouble());
        System.out.println(file.readUTF());
        file.skipBytes(3); // 将文件指针跳过3个字节,本例中即跳过了一个boolean值和short值。  
        System.out.println(file.readLong());
        file.skipBytes(file.readShort()); // 跳过文件中“又是一个UTF字符串”所占字节,注意readShort()方法会移动文件指针,所以不用加2。  
        System.out.println(file.readFloat());
        //以下演示文件复制操作  
        System.out.println("——————文件复制(从file到fileCopy)——————");
        file.seek(0);
        RandomAccessFile fileCopy = new RandomAccessFile("fileCopy", "rw");
        int len = (int) file.length(); //取得文件长度(字节数)  
        byte[] b = new byte[len];
        file.readFully(b);
        fileCopy.write(b);
        System.out.println("复制完成!");
    }
}

RandomAccessFile插入写示例: 

/** 
 *  
 * @param skip 跳过多少过字节进行插入数据 
 * @param str 要插入的字符串 
 * @param fileName 文件路径 
 */
public static void beiju(long skip, String str, String fileName) {
    try {
        RandomAccessFile raf = new RandomAccessFile(fileName, "rw");
        if (skip < 0 || skip > raf.length()) {
            System.out.println("跳过字节数无效");
            return;
        }
        byte[] b = str.getBytes();
        raf.setLength(raf.length() + b.length);
        for (long i = raf.length() - 1; i > b.length + skip - 1; i--) {
            raf.seek(i - b.length);
            byte temp = raf.readByte();
            raf.seek(i);
            raf.writeByte(temp);
        }
        raf.seek(skip);
        raf.write(b);
        raf.close();
    } catch(Exception e) {
        e.printStackTrace();
    }
}

利用RandomAccessFile实现文件的多线程下载,即多线程下载一个文件时,将文件分成几块,每块用不同的线程进行下载。下面是一个利用多线程在写文件时的例子,其中预先分配文件所需要的空间,然后在所分配的空间中进行分块,然后写入:

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
/** 
 * 测试利用多线程进行文件的写操作 
 */
public class Test {
    public static void main(String[] args) throws Exception {
        // 预分配文件所占的磁盘空间,磁盘中会创建一个指定大小的文件  
        RandomAccessFile raf = new RandomAccessFile("D://abc.txt", "rw");
        raf.setLength(1024 * 1024); // 预分配 1M 的文件空间  
        raf.close();
        // 所要写入的文件内容  
        String s1 = "第一个字符串";
        String s2 = "第二个字符串";
        String s3 = "第三个字符串";
        String s4 = "第四个字符串";
        String s5 = "第五个字符串";
        // 利用多线程同时写入一个文件  
        new FileWriteThread(1024 * 1, s1.getBytes()).start(); // 从文件的1024字节之后开始写入数据  
        new FileWriteThread(1024 * 2, s2.getBytes()).start(); // 从文件的2048字节之后开始写入数据  
        new FileWriteThread(1024 * 3, s3.getBytes()).start(); // 从文件的3072字节之后开始写入数据  
        new FileWriteThread(1024 * 4, s4.getBytes()).start(); // 从文件的4096字节之后开始写入数据  
        new FileWriteThread(1024 * 5, s5.getBytes()).start(); // 从文件的5120字节之后开始写入数据  
    }
    // 利用线程在文件的指定位置写入指定数据  
    static class FileWriteThread extends Thread {
        private int skip;
        private byte[] content;
        public FileWriteThread(int skip, byte[] content) {
            this.skip = skip;
            this.content = content;
        }
        public void run() {
            RandomAccessFile raf = null;
            try {
                raf = new RandomAccessFile("D://abc.txt", "rw");
                raf.seek(skip);
                raf.write(content);
            } catch(FileNotFoundException e) {
                e.printStackTrace();
            } catch(IOException e) {
                // TODO Auto-generated catch block  
                e.printStackTrace();
            } finally {
                try {
                    raf.close();
                } catch(Exception e) {}
            }
        }
    }
}

InputStream的三个read的区别

1.read
这个方法是对这个流一个一个字节的读,返回的int就是这个字节的int表示方式
以下是代码片段,经过测试当eclipse的编码为gbk时,转化出的字符串不需经过重新编码,如果eclipse的编码为utf-8时则由byte转成字符串需重新编成utf-8的

InputStream in = Test.class.getResourceAsStream("/tt.txt");
  byte[]tt=new byte[15];//测试用的事前知道有15个字节码
  while(in.available()!=0){
   
   for(int i=0;i<15;i++){
    tt[i]=(byte)in.read();
   }
  }
  String ttttt=new String(tt,"utf-8");
  System.out.println(ttttt);
  in.close();



2.read(byte[] b)
这个方法是先规定一个数组长度,将这个流中的字节缓冲到数组b中,返回的这个数组中的字节个数,这个缓冲区没有满的话,则返回真实的字节个数,到未尾时都返回-1

in = Test.class.getResourceAsStream("/tt.txt");
  byte [] tt=new byte[1024];
  int b;
  while((b=in.read(tt))!=-1){
   
   System.out.println(b);
  String tzt=new String(tt,"utf-8");
  
   
  System.out.println(tzt);



3.read(byte[] b, int off, int len)
此方法其实就是多次调用了read()方法

InputStream in = Test.class.getResourceAsStream("/tt.txt");
  //System.out.println(in.available());//此方法是返回这个流中有多少个字节数,可以把数组长度定为这个
  
  byte[]tt=new byte[in.available()];
  int z;
  while((z=in.read(tt, 0, tt.length))!=-1){
   System.out.println(new String(tt,"utf-8"));
  }

Intent 详解

一、Intent的用途

Intent主要有以下几种重要用途: 

1. 启动Activity:可以将Intent对象传递给startActivity()方法或startActivityForResult()方法以启动一个Activity,该Intent对象包含了要启动的Activity的信息及其他必要的数据。 

2. 启动Service:可以将Intent对象传递给startService()方法或bindService()方法以启动一个Service,该Intent对象包含了要启动的Service的信息及其他必要的数据。关于使用startService()方法启动Service,可以参见《Android中startService基本使用方法概述》。关于使用bindService()方法启动Service,可以参见《Android中bindService基本使用方法概述》。 

3. 发送广播:广播是一种所有App都可以接收的信息。Android系统会发布各种类型的广播,比如发布开机广播或手机充电广播等。我们也可以给其他的App发送广播,可以将Intent对象传递给sendBroadcast()方法或sendOrderedBroadcast()方法或sendStickyBroadcast()方法以发送自定义广播。

二、Intent的类型

有两种类型的Intent:explicit(显式)的和implict(隐式)的。

显式的Intent:如果Intent中明确包含了要启动的组件的完整类名(包名及类名),那么这个Intent就是explict的,即显式的。使用显式Intent最典型的情形是在你自己的App中启动一个组件,因为你自己肯定知道自己的要启动的组件的类名。比如,为了响应用户操作通过显式的Intent在你的App中启动一个Activity或启动一个Service下载文件。

隐式的Intent:如果Intent没有包含要启动的组件的完整类名,那么这个Intent就是implict的,即隐式的。虽然隐式的Intent没有指定要启动的组件的类名,但是一般情况下,隐式的Intent都要指定需要执行的action。一般,隐式的Intent只用在当我们想在自己的App中通过Intent启动另一个App的组件的时候,让另一个App的组件接收并处理该Intent。例如,你想在地图上给用户显示一个位置,但是你的App又不支持地图展示,这时候你可以将位置信息放入到一个Intent中,然后给它指定相应的action,通过这样隐式的Intent请求其他的地图型的App(例如Google Map、百度地图等)来在地图中展示一个指定的位置。隐式的Intent也体现了Android的一种设计哲学:我自己的App无需包罗万象所有功能,可以通过与其他App组合起来,给用户提供很好的用户体验。而连接自己的App与其他App的纽带就是隐式Intent。

当创建了一个显式Intent去启动Activity或Service的时候,系统会立即启动Intent中所指定的组件。

当创建了一个隐式Intent去使用的时候,Android系统会将该隐式Intent所包含的信息与设备上其他所有App中manifest文件中注册的组件的Intent Filters进行对比过滤,从中找出满足能够接收处理该隐式Intent的App和对应的组件。如果有多个App中的某个组件都符合条件,那么Android会弹出一个对话框让用户选择需要启动哪个App。

Intent Filter,即Intent过滤器,一个组件可以包含0个或多个Intent Filter。Intent Filter是写在App的manifest文件中的,其通过设置action或uri数据类型等指明了组件能够处理接收的Intent的类型。如果你给你的Activity设置了Intent Filter,那么这就使得其他的App有可能通过隐式Intent启动你的这个Activity。反之,如果你的Activity不包含任何Intent Filter,那么该Activity只能通过显式Intent启动,由于我们一般不会暴露出我们组件的完整类名,所以这种情况下,其他的App基本就不可能通过Intent启动我们的Activity了(因为他们不知道该Activity的完整类名),只能由我们自己的App通过显式Intent启动。

需要注意的是,为了确保App的安全性,我们应该总是使用显式Intent去启动Service并且不要为该Service设置任何的Intent Filter。通过隐式的Intent启动Service是有风险的,因为你不确定最终哪个App中的哪个Service会启动起来以响应你的隐式Intent,更悲催的是,由于Service没有UI的在后台运行,所以用户也不知道哪个Service运行了。从Android 5.0 (API level 21)开始,用隐式Intent调用bindService()方法,Android会抛出异常,但是也有相应技巧,将一个隐式的Intent转换为显式的Intent,然后用显式的Intent去调用bindService()方法就没有问题了,具体解决办法可以参见博文《Android中通过Messenger与Service实现进程间双向通信》中最后的“注意事项”部分,里面有相关代码的解决方案。

三、Intent的组成

Android可以根据Intent所携带的信息去查找要启动的组件,Intent还携带了一些数据信息以便要启动的组件根据Intent中的这些数据做相应的处理。

Intent由6部分信息组成:Component Name、Action、Data、Category、Extras、Flags。根据信息的作用用于,又可分为三类: 

a. Component Name、Action、Data、Category为一类,这4中信息决定了Android会启动哪个组件,其中Component Name用于在显式Intent中使用,Action、Data、Category、Extras、Flags用于在隐式Intent中使用。 

b. Extras为一类,里面包含了具体的用于组件实际处理的数据信息。 

c. Flags为一类,其是Intent的元数据,决定了Android对其操作的一些行为,下面会介绍。

Component name 

要启动的组件的名称。如果你想使用显式的Intent,那么你就必须指定该参数,一旦设置了component name,Android会直接将Intent传递给组件名所指定的组件去启动它。如果没有设置component name,那么该Intent就是隐式的,Android系统会根据其他的Intent的信息(例如下面要介绍到的action、data、category等)做一些比较判断决定最终要启动哪个组件。所以,如果你启动一个你自己App中的组件,你应该通过指定component name通过显式Intent去启动它(因为你知道该组件的完整类名)。

需要注意的是,当启动Service的时候,你应该总是指定Component Name。否则,你不确定最终哪个App的哪个组件被启动了,并且用户也看不到哪个Service启动了。

component name在Intent中对应的field是ComponentName对象,你可以通过要启动的组件的完整类名(包括应用的包名)指定该值,例如com.example.ExampleActivity。你可以通过Intent的setComponent()方法、setClass()方法、setClassName()方法或Intent的构造函数指定component name。

Action 

是表示了要执行操作的字符串,比如查看或选择,其对应着Intent Filter中的action标签<action />。

你可以指定你独有的action以便于你的App中的Intent的使用或其他App中通过Intent调用你的App中的组件。Intent类和Android中其他framework级别的一些类也提供了许多已经定义好的具有一定通用意义的action。以下是一些用于启动Activity的常见的action: 

Intent.ACTION_VIEW 其值为 “android.intent.action.VIEW”,当你有一些信息想让通过其他Activity展示给用户的时候,你就可以将Intent的action指定为ACTION_VIEW,比如在一个图片应用中查看一张图片,或者在一个地图应用中展现一个位置。 

Intent.ACTION_SEND 其值为”android.intent.action.SEND”,该action常用来做“分享”使用,当你有一些数据想通过其他的App(例如QQ、微信、百度云等)分享出去的时候,就可以使用此action构建Intent对象,并将其传递给startActivity()方法,由于手机上可能有多个App的Activity均支持ACTION_SEND这一action,所以很有可能会出现如下的图片所示的情形让用户具体选择要通过哪个App分享你的数据:

可以通过查看Intent类了解更多的Intent预定义的一些常见的action。Android中framework级别的一些类也定义了一些action,例如Settings中定义了一些action用以分别打开系统中“设置”这个应用的不同界面以完成对指定配置(如WLAN设置、语言设置等)。

你可以通过调用intent对象的setAction()方法或在Intent的构造函数中指定intent的action。

如果你定义了你自己的action,请务必将你的App的包名作为该action的前缀,这是一种良好的编程习惯,避免造成混淆,例如:

复制代码 代码如下:

static final String ACTION_TIMETRAVEL = "com.example.action.TIMETRAVEL";

Data 

此处所说的Intent中的data指的是Uri对象和数据的MIME类型,其对应着Intent Filter中的data标签<data />。 

一个完整的Uri由scheme、host、port、path组成,格式是<scheme>://<host>:<port>/<path>,例如content://com.example.project:200/folder/subfolder/etc。Uri就像一个数据链接,组件可以根据此Uri获得最终的数据来源。通常将Uri和action结合使用,比如我们将action设置为ACTION_VIEW,我们应该提供将要被编辑修改的文档的Uri。

当创建了一个Intent对象的时候,除了指定Uri之外,指定数据的MIME类型也很重要。例如,一个Activity能够显示图片,但是不能够播放视频,显示图片的Uri和播放视频的Uri可能很类似,为了不让Android误将一个含有视频Uri的Intent对象传递给一个只能显示图片的Activity,我们需要在该Activity的Intent Filter中指定MIME类型为图片(例如<data android:mimeType="image/*" … />)并且还要给Intent对象设置对应的图片类型的MIME,这样Android就会基于Uri和MIME类型将Intent传递给符合条件的组件。然后有个特例,如果Uri使用的是content:协议,那么这就说明Uri所提供的数据将来自于本地设备,即数据由ContentProvider提供,这种情况下Android会根据Uri自动推断出MIME类型,此种情况我们无需再自己指定MIME类型。

如果只设置数据的Uri,需要调用Intent对象的setData()方法;如果只设置数据的MIME类型,需要调用Intent对象的setType()方法;如果要同时设置数据的Uri和MIME类型,需要调用Intent对象的setDataAndType()方法。

需要注意的是,如果你想要同时设置数据的Uri和MIME类型,不要先后调用Intent对象的setData()方法和setType()方法,因为setData()方法和setType()是互斥的,即如果调用了setData()方法,会将Intent中已经通过setType()方法设置的MIME类型重置为空。如果调用了setType()方法,会将Intent中已经通过setData()方法设置的Uri重置为空。所以在需要同时设置数据的Uri和MIME类型的时候,一定要调用Intent对象的setDataAndType()方法,而不是分别调用setData()方法和setType()方法。

Category 

category包含了关于组件如何处理Intent的一些其他信息,虽然可以在Intent中加入任意数量的category,但是大多数的Intent其实不需要category。 

以下是一些常见的category:

CATEGORY_BROWSABLE 目标组件会允许自己通过一个链接被一个Web浏览器启动,该链接可能是一个图片链接或e-mail信息等。

CATEGORY_LAUNCHER 用于标识Activity是某个App的入口Activity。

你可以在Intent类中查找到更多预定义的category。

Extras 

extras,顾名思义,就是额外的数据信息,Intent中有一个Bundle对象存储着各种键值对,接收该Intent的组件可以从中读取出所需要的信息以便完成相应的工作。有的Intent需要靠Uri携带数据,有的Intent是靠extras携带数据信息。

你可以通过调用Intent对象的各种重载的putExtra(key, value)方法向Intent中加入各种键值对形式的额外数据。你也可以直接创建一个Bundle对象,向该Bundle对象传入很多键值对,然后通过调用Intent对象的putExtras(Bundle)方法将其一块设置给Intent对象中去。

例如,你创建了一个action为ACTION_SEND的Intent对象,然后想用它启动e-mail发送邮件,那么你需要给该Intent对象设置两个extra的值: 

用Intent.EXTRA_EMAIL 作为key值设置收件方,用Intent.EXTRA_SUBJECT 作为key值设置邮件标题。

Intent类里面也指定了很多预定义的EXTRA_*形式的extra,例如上面我们提到的(Intent.EXTRA_EMAIL 和Intent.EXTRA_SUBJECT)。如果你想要声明你自己自定义的extra,请确保将你的App的包名作为你的extra的前缀,例如:

复制代码 代码如下:

static final String EXTRA_GIGAWATTS = "com.example.EXTRA_GIGAWATTS";

Flags 

flag就是标记的意思,Intent类中定义的flag能够起到作为Intent对象的元数据的作用。这些flag会告知Android系统如何启动Activity(例如,新启动的Activity属于哪个task)以及在该Activity启动后如何对待它(比如)。更多信息可参见Intent的setFlags()方法。

1、显式Intent使用示例

Intent intent = new Intent(this, ActivityB.class);
startActivity(intent);

上面的代码在Intent的构造函数中指定了要启动的组件的ComponentName是ActivityB,该intent对象是显式的,调用startActivity(intent)时,Android系统会立即启动ActivityB。

2、隐式Intent使用示例

之前提到过,在使用隐式Intent的时候需要指定其action。如果你的App不能完成某个功能,但是其他的App可能完成该功能,那么你就可以用隐式Intent启动其他的App去完成相应的功能。例如,你有一段文本信息,想通过其他App分享出去,那么隐式Intent对象去启动潜在的支持分享的App,示例代码如下:

Intent sendIntent = new Intent();
// 设置action, action对隐式Intent来说是非常重要的
sendIntent.setAction(Intent.ACTION_SEND);
// 设置数据的MIME类型为纯文本类型
sendIntent.setType("text/plain");
// 设置额外的数据
sendIntent.putExtra(Intent.EXTRA_TEXT, textMessage);
// 获取包管理器
PackageManager pm = getPackageManager();
// 先判断系统中有没有潜在的App的Activity支持对该sendIntent的接收与处理
if (pm.resolveActivity(sendIntent, 0) != null) {
 startActivity(sendIntent);
}

上面的代码中,我们构建了一个Intent对象,并没有给其设置component name,所以该Intent是一个隐式的Intent对象。我们首先给intent设置了action的值为Intent.ACTION_SEND,action对隐式Intent来说是非常重要的。然后我们将intent的数据的MIME类型设置为纯文本类型(“text/plain”),告知Android我们的Intent持有的是文本类型的数据。最后我们将实际的文本数据通过putExtra()方法作为额外数据设置进去。 

需要注意的是,在构建好了Intent对象之后,我们没有立即执行startActivity(sendIntent)方法,而是将sendIntent作为参数传递给了PackageManager的resolveActivity()方法中,该方法会让Android根据该sendIntent找到潜在的适合启动的组件的信息,并以ResolveInfo类的对象的形式返回结果,如果返回null,表示当前系统中没有任何组件可以接收并处理该sendIntent。如果返回不是null,就表明系统中至少存在一个组件可以接收并处理该sendIntent,只有在这种情况下,我们才会执行代码startActivity(sendIntent),在通过intent启动组件之前先判断要启动的组件存不存在是个良好的编程习惯,因为如果系统中不存在支持你的intent的组件,那么当你调用startActivity()、startService()、bindService()等方法的时候,Android就会抛出异常。

四、强制用户使用App Chooser

在上文中我们已经提到,如果我们的Intent是隐式的,当我们通过startActivity(intent)尝试启动组件的时候,可能Android系统会显示上面的截图文件询问用户要启动哪个App,有时候用户会将某一个App设置为默认的App,这样下次我们再执行代码startActivity(intent)的时候就有可能不会再出现选择App的界面,而是直接运行上次用户设置为默认App的应用。这对于用户选择一个默认浏览器打开网页这种情形是有好处的,因为一般一个用户习惯于用一个自己喜欢的浏览器。

但是如果用户不想每次都用同一个默认App处理这样的情形怎么办呢?这时候我们可以在代码中明确地使用App选择对话框,比如党我们的App执行一个action为ACTION_SEND的分享功能时,我们想让用户分享自己数据的代码,但是我们不确定用户想通过哪个App去分享,我们想每次都弹出App选择对话框让用户决定想通过哪个App分享,示例代码如下所示:

Intent sendIntent = new Intent(Intent.ACTION_SEND);
String title = "请选择想通过哪个App分享数据";
// 验证是否有App能够接收并处理sendIntent
if (sendIntent.resolveActivity(getPackageManager()) != null) {
 // 根据sendIntent创建一个需要显示App选择对话框的intent对象
 Intent chooserIntent = Intent.createChooser(sendIntent, title);
 // 我们使用chooserIntent作为startActivity()方法的参数,而非sendIntent
 startActivity(chooserIntent);
}

首先我们创建了我们原始的sendIntent,并对其设置action等相关信息,然后我们将sendIntent传递给了Intent.createChooser()方法中,创建了另一个chooserIntent。后面我们通过调用Intent.resolveActivity(PackageManager)方法判断系统中是否有App能够接收并处理sendIntent,该方法与上面之前提到过的PackageManager的resolveActivity()方法是等价的。最后我们使用chooserIntent作为startActivity()方法的参数,而非sendIntent,chooserIntent会让Android系统强制显示用户选择App处理Intent的界面。

本文大部分参考了Android中对Intent部分的Develop Guide的描述

java中的匿名内部类总结

匿名内部类也就是没有名字的内部类

正因为没有名字,所以匿名内部类只能使用一次,它通常用来简化代码编写

但使用匿名内部类还有个前提条件:必须继承一个父类或实现一个接口

实例1 : 不使用匿名内部类来实现抽象方法

abstract class Person {
    public abstract void eat();
}

class Child extends Person {
    public void eat() {
        System.out.println("eat something");
    }
}

public class Demo {
    public static void main(String[] args) {
        Person p = new Child();
        p.eat();
    }
}

运行结果:eat something

可以看到,我们用Child继承了Person类,然后实现了Child的一个实例,将其向上转型为Person类的引用

但是,如果此处的Child类只使用一次,那么将其编写为独立的一个类岂不是很麻烦?

这个时候就引入了匿名内部类

实例2:匿名内部类的基本实现

abstract class Person {
    public abstract void eat();
}

public class Demo {
    public static void main(String[] args) {
        Person p = new Person() {
            public void eat() {
                System.out.println("eat something");
            }
        };
        p.eat();
    }
}

运行结果:eat something

可以看到,我们直接将抽象类Person中的方法在大括号中实现了

这样便可以省略一个类的书写

并且,匿名内部类还能用于接口上

实例3:在接口上使用匿名内部类

interface Person {
    public void eat();
}
public class Demo {
    public static void main(String[] args) {
        Person p = new Person() {
            public void eat() {
                System.out.println("eat something");
            }
        };
        p.eat();
    }
}

运行结果:eat something

由上面的例子可以看出,只要一个类是抽象的或是一个接口,那么其子类中的方法都可以使用匿名内部类来实现

最常用的情况就是在多线程的实现上,因为要实现多线程必须继承Thread类或是继承Runnable接口

实例4:Thread类的匿名内部类实现

public class Demo {
    public static void main(String[] args) {
        Thread t = new Thread() {
            public void run() {
                for (int i = 1; i <= 5; i++) {
                    System.out.print(i + " ");
                }
            }
        };
        t.start();
    }
}

运行结果:1 2 3 4 5

实例5:Runnable接口的匿名内部类实现

public class Demo {
    public static void main(String[] args) {
        Runnable r = new Runnable() {
            public void run() {
                for (int i = 1; i <= 5; i++) {
                    System.out.print(i + " ");
                }
            }
        };
        Thread t = new Thread(r);
        t.start();
    }
}

向下转型

在学习Android的时候 书里提到了向下转型

对java不熟悉的我又要补课了

这篇博客就是讲向下转型的,那我们就来学习下向下转型,了解下这种特性的意义和使用场景

新建一个电子产品接口,如下:

public interface Electronics{

}

很简单,什么方法都没有。

新建一个Thinkpad笔记本类,并实现电子产品接口:

public class Thinkpad implements Electronics {

    //Thinkpad引导方法
    public void boot() {
        System.out.println("welcome,I am Thinkpad");
    }

    //使用Thinkpad编程  
    public void program() {
        System.out.println("using Thinkpad program");
    }

}

新建一个Mouse鼠标类,并实现电子产品接口:

public class Mouse implements Electronics{

    //鼠标移动
    public void move(){
        System.out.println("move the mouse");       
    }    //鼠标点击  
    public void onClick(){
        System.out.println("a click of the mouse");
    }

}

新建一个Keyboard键盘类,并实现电子产品接口:

public class Keyboard implements Electronics{

    //使用键盘输入    
    public void input(){
        System.out.println("using Keyboard input");
    }

}

这里子类比较多,是为了更好的理解。每个类的方法的逻辑实现也很简单。打印了一行信息

接下来,我们想象一个情景:我们去商城买电子产品,电子产品很多吧,比如笔记本电脑,鼠标,键盘,步步高点读机哪里不会点哪里,我们用的手机,等等,这些都属于电子产品。

电子产品是抽象的。

好,那么我们决定买一台Thinkpad,一个鼠标和一个键盘。

这时,我们需要一个购物车来装这些电子产品吧。

我们可以添加进购物车,然后通过购物车还能知道存放的电子产品数量,能拿到对应的电子产品。

那么,一个购物车类就出来了,如下:

import java.util.ArrayList;
import java.util.List;

public class ShopCar {

    private List < Electronics > mlist = new ArrayList < Electronics > ();

    public void add(Electronics electronics) {

        mlist.add(electronics);

    }

    public int getSize() {

        return mlist.size();
    }

    public Electronics getListItem(int position) {

        return mlist.get(position);

    }

}

List集合是用来存放电子产品的,add方法用来添加电子产品到购物车,getSize方法用来获取存放的电子产品数量,getListItem方法用来获取相应的电子产品。

可以看到List < Electronics > 用了泛型的知识,至于为什么要用泛型?

这个不做介绍了,泛型很重要的。

而我觉得比较疑惑的是为什么是放Electronics的泛型,而不是放Thinkpad,Mouse,Keyboard,Phone等?

那么如果是List < Thinkpad > ,肯定是放不进鼠标Mouse的吧,难道要生成3个集合?

这里是定义了3个电子产品类,但是我如果有100种电子产品呢,要定义100个集合 ? 

这太可怕了。

所以之前,我们写了一个Electronics接口,提供了一个Electronics的标准,然后让每一个Electronics子类都去实现这个接口。

实际上这里又涉及到了向上转型的知识点,我们虽然在add方法将子类实例传了进来存放,但子类实例在传进去的过程中也进行了向上转型所以,此时购物车里存放的子类实例对象,由于向上转型成Electronics,已经丢失了子类独有的方法,以上述例子来分析,Thinkpad实例就是丢失了boot()和program()这两个方法,而Mouse实例就是丢失了move()和onClick()这两个方法

但是实际使用Thinkpad或Mouse或Keyboard时,这种情况肯定不是我们想要的

接着我们写一个测试类Test去测试购物车里的电子产品。

测试类Test如下:

public class Test {

    public static final int THINKPAD = 0;
    public static final int MOUSE = 1;
    public static final int KEYBOARD = 2;

    public static void main(String[] args) {

        //添加进购物车
        ShopCar shopcar = new ShopCar();
        shopcar.add(new Thinkpad());
        shopcar.add(new Mouse());
        shopcar.add(new Keyboard());

        //获取大小
        System.out.println("购物车存放的电子产品数量为 ——> " + shopcar.getSize());

        //开始测试thinkpad电脑
        Thinkpad thinkpad = (Thinkpad) shopcar.getListItem(THINKPAD);
        thinkpad.boot();
        thinkpad.program();

        System.out.println("-------------------");

        //开始测试Mouse鼠标
        Mouse mouse = (Mouse) shopcar.getListItem(MOUSE);
        mouse.move();
        mouse.onClick();

        System.out.println("-------------------");

        //开始测试Keyboard键盘
        Keyboard keyboard = (Keyboard) shopcar.getListItem(KEYBOARD);
        keyboard.input();
    }

}

运行截图:

20161022214436745.png

举个例子分析就好

//开始测试thinkpad电脑
Thinkpad thinkpad = (Thinkpad)shopcar.getListItem(THINKPAD);
thinkpad.boot();
thinkpad.program();

shopcar.getListItem(THINKPAD)这句代码是获取到Electronics类型的实例。不是Thinkpad的实例

通过向下转型,赋值给子类引用

Thinkpad thinkpad = (Thinkpad) shopcar.getListItem(THINKPAD);

这样子类实例又重新获得了因为向上转型而丢失的方法(boot和program)

总结一下吧,很多时候,我们需要把很多种类的实例对象,全部扔到一个集合。(这句话很重要)

在这个例子里就是把Thinkpad笔记本,Mouse鼠标,KeyBoard键盘等实例对象,全部扔到一个Shopcar购物车集合。

但是肯定不可能给他们每个种类都用一个独立的集合去存放吧,这个时候我们应该寻找到一个标准,接口就是一个标准。这些都是各种电子产品,抽象成电子产品。

然后一个Electronics接口就出来了。

在回到刚才,我们把很多种类的实例对象全部扔到一个集合。

或许这样比较好理解:把很多种类的子类实例对象全部扔到存放父类实例的集合。

经过了这个过程,子类实例已经赋值给了父类引用(即完成了向上转型),但很遗憾的丢失了子类扩展的方法。

很好的是Java语言有个向下转型的特性,让我们可以重新获得丢失的方法,即强转回子类所以我们需要用到子类实例的时候,就从那个父类集合里拿出来向下转型就可以了,一样可以使用子类实例对象……

我在搜索java向下转型的意义时,得到一个比较好的答案是这样的:最大的用处是java的泛型编程,用处很大,Java的集合类都是这样的。

而在Android开发中,我们在Layout文件夹,用xml写的控件。为什么能在Activity等组件中通过findViewById()方法找到呢?

为什么findViewById(R.id.textview)方法传入TextView的id后,还要转型为TextView呢?

这就是Java向下转型的一个应用。

JAVA 向上/向下转型

向上转型


我们在现实中常常这样说:这个人会唱歌。

在这里,我们并不关心这个人是黑人还是白人,

是成人还是小孩,也就是说我们更倾向于使用抽象概念“人”。

再例如,麻雀是鸟类的一种(鸟类的子类),

而鸟类则是动物中的一种(动物的子类)。

我们现实中也经常这样说:麻雀是鸟。

这两种说法实际上就是所谓的向上转型,

通俗地说就是子类转型成父类。

这也符合Java提倡的面向抽象编程思想。来看下面的代码:

package a.b;
public class A {
    public void a1() {
        System.out.println("Superclass");
    }
}


A的子类B:

package a.b;

public class B extends A {

    public void a1() {

        System.out.println("Childrenclass"); //覆盖父类方法
    }

    public void b1() {} //B类定义了自己的新方法
}


C类:

package a.b;

public class C {

    public static void main(String[] args) {

        A a = new B(); //向上转型
        a.a1();

    }

}


如果运行C,输出的是Superclass 还是Childrenclass?

不是你原来预期的Superclass,而是Childrenclass。

这是因为a实际上指向的是一个子类对象。

当然,你不用担心,Java虚拟机会自动准确地识别出究竟该调用哪个具体的方法。

不过,由于向上转型,a对象会遗失和父类不同的方法,例如b1()。

有人可能会提出疑问:这不是多此一举吗?我们完全可以这样写:

B a = new B();

a.a1();


确实如此!但这样就丧失了面向抽象的编程特色,降低了可扩展性。其实,不仅仅如此,向上转型还可以减轻编程工作量。

来看下面的显示器类Monitor:

package a.b;

public class Monitor {

    public void displayText() {}

    public void displayGraphics() {}

}


液晶显示器类LCDMonitor是Monitor的子类:

package a.b;

public class LCDMonitor extends Monitor {

    public void displayText() {

        System.out.println("LCD display text");

    }

    public void displayGraphics() {

        System.out.println("LCD display graphics");

    }

}


阴极射线管显示器类CRTMonitor自然也是Monitor的子类:

package a.b;

public class CRTMonitor extends Monitor {

    public void displayText() {

        System.out.println("CRT display text");

    }

    public void displayGraphics() {

        System.out.println("CRT display graphics");

    }

}


等离子显示器PlasmaMonitor也是Monitor的子类:

package a.b;

public class PlasmaMonitor extends Monitor {

    public void displayText() {

        System.out.println("Plasma display text");

    }

    public void displayGraphics() {

        System.out.println("Plasma display graphics");

    }

}


现在有一个MyMonitor类。假设没有向上转型,MyMonitor类代码如下:

package a.b;

public class MyMonitor {

    public static void main(String[] args) {

        run(new LCDMonitor());

        run(new CRTMonitor());

        run(new PlasmaMonitor());

    }

    public static void run(LCDMonitor monitor) {

        monitor.displayText();

        monitor.displayGraphics();

    }

    public static void run(CRTMonitor monitor) {

        monitor.displayText();

        monitor.displayGraphics();

    }

    public static void run(PlasmaMonitor monitor) {

        monitor.displayText();

        monitor.displayGraphics();

    }

}


可能你已经意识到上述代码有很多重复代码,而且也不易维护。有了向上转型,代码可以更为简洁:

package a.b;

public class MyMonitor {

    public static void main(String[] args) {

        run(new LCDMonitor()); //向上转型
        run(new CRTMonitor()); //向上转型
        run(new PlasmaMonitor()); //向上转型
    }

    public static void run(Monitor monitor) { //父类实例作为参数
        monitor.displayText();

        monitor.displayGraphics();

    }

}

向下转型


子类转型成父类是向上转型,反过来说,父类转型成子类就是向下转型。

但是,向下转型可能会带来一些问题:我们可以说麻雀是鸟,但不能说鸟就是麻雀。来看下面的例子:


A类:

package a.b;

public class A {

    void aMthod() {

        System.out.println("A method");

    }

}

A的子类B:

package a.b;

public class B extends A {

    void bMethod1() {

        System.out.println("B method 1");

    }

    void bMethod2() {

        System.out.println("B method 2");

    }

}

C类:

package a.b;

public class C {
    public static void main(String[] args) {
        A a1 = new B(); // 向上转型
        a1.aMthod(); // 调用父类aMthod(),a1遗失B类方法bMethod1()、bMethod2()
        
        B b1 = (B) a1; // 向下转型,编译无错误,运行时无错误
        b1.aMthod(); // 调用父类A方法
        b1.bMethod1(); // 调用B类方法
        b1.bMethod2(); // 调用B类方法
        
        A a2 = new A();
        B b2 = (B) a2; // 向下转型,编译无错误,运行时将出错
        b2.aMthod();
        b2.bMethod1();
        b2.bMethod2();
    }

}


从上面的代码我们可以得出这样一个结论:向下转型需要使用强制转换。

运行C程序,控制台将输出:

Exception in thread "main"java.lang.ClassCastException: a.b.A cannot be cast to a.b.B at a.b.C.main(C.java: 14)

A method

A method

B method 1

B method 2

其实黑体部分的向下转型代码后的注释已经提示你将发生运行时错误。

为什么前一句向下转型代码可以,而后一句代码却出错?

这是因为a1指向一个子类B的对象,

所以子类B的实例对象b1当然也可以指向a1。

而a2是一个父类对象,子类对象b2不能指向父类对象a2。

那么如何避免在执行向下转型时发生运行时ClassCastException异常?

使用instanceof就可以了。

我们修改一下C类的代码:

A a2 = new A();

if (a2 instanceof B) {

    B b2 = (B) a2;

    b2.aMthod();

    b2.bMethod1();

    b2.bMethod2();

}

这样处理后,就不用担心类型转换时发生ClassCastException异常了。