全文检索框架Lucene——Demo应用

前面两篇博文介绍了Lucene的原理思想和架构,这篇博文就写一下实际开发中如何使用Lucene,下面先用一个最简单的例子描述一下。


1. 建立索引

为了简单起见,我们下面为一些字符串创建内存索引:

StandardAnalyzer analyzer = newStandardAnalyzer(Version.LUCENE_40);
Directory index = newRAMDirectory();
 
IndexWriterConfig config = newIndexWriterConfig(Version.LUCENE_40, analyzer);
 
IndexWriter w = newIndexWriter(index, config);
addDoc(w,"Lucene in Action","193398817");
addDoc(w,"Lucene for Dummies","55320055Z");
addDoc(w,"Managing Gigabytes","55063554A");
addDoc(w,"The Art of Computer Science","9900333X");
w.close();

addDoc()方法把文档(译者注:这里的文档是Lucene中的Document类的实例)添加到索引中。

private static void addDoc(IndexWriter w, String title, String isbn) throws IOException {
  Document doc = newDocument();
  doc.add(newTextField("title", title, Field.Store.YES));
  doc.add(newStringField("isbn", isbn, Field.Store.YES));
  w.addDocument(doc);
}

注意,对于需要分词的内容我们使用TextField,对于像id这样不需要分词的内容我们使用StringField。

2.搜索请求

我们从标准输入(stdin)中读入搜索请求,然后对它进行解析,最后创建一个Lucene中的Query对象.

String querystr = args.length > 0? args[0] : "lucene";
Query q = newQueryParser(Version.LUCENE_40,"title", analyzer).parse(querystr);

3.搜索

我们创建一个Searcher对象并且使用上面创建的Query对象来进行搜索,匹配到的前10个结果封装在TopScoreDocCollector对象里返回。

int hitsPerPage = 10;
IndexReader reader = IndexReader.open(index);
IndexSearcher searcher = new IndexSearcher(reader);
TopScoreDocCollector collector = TopScoreDocCollector.create(hitsPerPage, true);
searcher.search(q, collector);
ScoreDoc[] hits = collector.topDocs().scoreDocs;


4.展示

现在我们得到了搜索结果,我们需要想用户展示它。

System.out.println("Found " + hits.length + " hits.");
for(int i=0; i<hits.length; ++i) {
    int docId = hits[i].doc;
    Document d = searcher.doc(docId);
    System.out.println((i + 1) + ". " + d.get("isbn") + "\t"+ d.get("title"));
}

这里是这个小应用的完整代码。下载HelloLucene.java

public class HelloLucene {
  public static void main(String[] args) throws IOException, ParseException {
    // 0. Specify the analyzer for tokenizing text.
    //    The same analyzer should be used for indexing and searching
    StandardAnalyzer analyzer = new StandardAnalyzer(Version.LUCENE_40);
 
    // 1. create the index
    Directory index = newRAMDirectory();
    IndexWriterConfig config = newIndexWriterConfig(Version.LUCENE_40, analyzer);
    IndexWriter w = newIndexWriter(index, config);
    addDoc(w,"Lucene in Action","193398817");
    addDoc(w,"Lucene for Dummies","55320055Z");
    addDoc(w,"Managing Gigabytes","55063554A");
    addDoc(w,"The Art of Computer Science","9900333X");
    w.close();
 
    // 2. query
    String querystr = args.length > 0? args[0] : "lucene";
 
    // the "title" arg specifies the default field to use
    // when no field is explicitly specified in the query.
    Query q = newQueryParser(Version.LUCENE_40,"title", analyzer).parse(querystr);
 
    // 3. search
    inthitsPerPage = 10;
    IndexReader reader = DirectoryReader.open(index);
    IndexSearcher searcher = newIndexSearcher(reader);
    TopScoreDocCollector collector = TopScoreDocCollector.create(hitsPerPage, true);
    searcher.search(q, collector);
    ScoreDoc[] hits = collector.topDocs().scoreDocs;
     
    // 4. display results
    System.out.println("Found " + hits.length + " hits.");
    for(int i=0; i<hits.length; ++i) {
      int docId = hits[i].doc;
      Document d = searcher.doc(docId);
      System.out.println((i + 1) + ". " + d.get("isbn") + "\t"+ d.get("title"));
    }
 
    // reader can only be closed when there
    // is no need to access the documents any more.
    reader.close();
  }
 
  private static void addDoc(IndexWriter w, String title, String isbn) throws IOException {
    Document doc = new Document();
    doc.add(new TextField("title", title, Field.Store.YES));
 
    // use a string field for isbn because we don't want it tokenized
    doc.add(new StringField("isbn", isbn, Field.Store.YES));
    w.addDocument(doc);
  }


 
通过上面你的Demo例子,我想你一定对Lucene有一定了解了,下面来总结一下Lucene的使用。

一、环境

需要导入lucene.jar包(在lucene.apache.org下载)

二、基本概念

1.Lucene的工作流程:

(1) 使用IndexWriter,在指定的目录建立索引的文件

(2) 将需要检索的数据转换位Document的Filed对象,然后将Document用IndexWriter添加倒索引的文件中

    (3) 处理索引信息,关闭IndexWriter流

    (4) 创建搜索的Query

    (5) 给IndexSearcher

2.Lucene的字段类型

Lucene有四种不同的字段类型:Keyword,UnIndexed,UnStored和Text,用于指定建立最佳索引。

(1)Keyword字段是指不需要分析器解析但需要被编入索引并保存到索引中的部分。JavaSourceCodeIndexer类使用该字段来保存导入类的声明。

(2)UnIndexed字段是既不被分析也不被索引,但是要被逐字逐句的将其值保存到索引中。由于我们一般要存储文件的位置但又很少用文件名作为关键字来搜索,所以用该字段来索引Java文件名。

(3)UnStored字段和UnIndexed字段相反。该类型的Field要被分析并编入索引,但其值不会被保存到索引中。由于存储方法的全部源代码需要大量的空间。所以用UnStored字段来存储被索引的方法源代码。可以直接从Java源文件中取出方法的源代码,这样作可以控制我们的索引的大小。

(4)Text字段在索引过程中是要被分析、索引并保存的。类名是作为Text字段来保存。下表展示了JavaSourceCodeIndexer类使用Field字段的一般情况。

3.基本概念(与传统表的对比)

Lucene概念

传统概念

备注

IndexWriter

table

Document

一条记录

Field

每个字段

分为可被索引的,可切分的,不可被切分的,不可被索引的几种组合类型

Hits

RecoreSet

结果集

IndexWriter提供了一些参数可供设置,列表如下:

(1).mergeFactory: 控制index的大小和频率,默认值:10,两个作用:1.一个段有多少document,2.多少个段合成一个大段

(2).maxMergeDocs:限制一个段中的document数目,默认值:Integer.MAX_VALUE

(3).minMergeDocs:缓存在内存中的document数目,超过他以后会写入到磁盘,默认值:10

(4).maxFieldLength:一个Field中最大Term数目,超过部分忽略,不会index到field中,所以自然也就搜索不到,默认值:1000

这些参数的的详细说明比较复杂:mergeFactor有双重作用

(1)设置每mergeFactor个document写入一个段,比如每10个document写入一个段

(2)设置每mergeFacotr个小段合并到一个大段,比如10个document的时候合并为1小段,以后有10个小段以后合并到一个大段,有10个大段以后再合并,实际的document数目会是mergeFactor的指数

简单的来说mergeFactor 越大,系统会用更多的内存,更少磁盘处理,如果要打批量的作index,那么把mergeFactor设置大没错, mergeFactor 小了以后, index数目也会增多,searhing的效率会降低,但是mergeFactor增大一点一点,内存消耗会增大很多(指数关系),所以要留意不要”out of memory”

把maxMergeDocs设置小,可以强制让达到一定数量的document写为一个段,这样可以抵消部分mergeFactor的作用.

minMergeDocs相当于设置一个小的cache,第一个这个数目的document会留在内存里面,不写入磁盘。这些参数同样是没有最佳值的,必须根据实际情况一点点调整。

maxFieldLength可以在任何时刻设置,设置后,接下来的index的Field会按照新的length截取,之前已经index的部分不会改变。可以设置为Integer.MAX_VALUE

4.几种查询方式      

查询方式

说明

TermQuery           

条件查询

例如:TermQuery tquery=new TermQuery(new Term("name","jerry"));

name:字段名

jerry:要搜索的字符串

MultiTermQuery

多个字段进行同一关键字的查询

Query query= null;

Query =MultiFieldQueryParser.parse("我",new String[] 学习lucene - 风 - 杨超峰的博客{"title","content"},analyzer);

Searcher searcher=new IndexSearcher(indexFilePath);

Hits hits=searcher.search(query);

BooleanQuery

例如:BooleanQuery bquery=new BooleanQuery();

学习lucene - 风 - 杨超峰的博客 bquery.add(query,true,false);

    bquery.add(mquery,true,false);

    bquery.add(tquery,true,false);

    Searcher searcher=new IndexSearcher(indexFilePath);

     Hits hits=searcher.search(bquery);

WildcardQuery

语义查询(通配符查询)

例:Query query= new WildcardQuery(new Term("sender","*davy*"));

PhraseQuery

短语查询

PrefixQuery

前缀查询

PhrasePrefixQuery

短语前缀查询

FuzzyQuery

模糊查询

RangeQuery

范围查询

SpanQuery

范围查询

在全文检索时建议大家先采用语义时的搜索,先搜索出有意义的内容,之后再进行模糊之类的搜索

(1)联合两个索引查询,已解决:

IndexSearcher[] searchers = new IndexSearcher[2];  

    searchers[0] = new IndexSearcher(m_indexpath);

    searchers[1] = new IndexSearcher(m_outindexpath);

    MultiSearcher multiSearcher = new MultiSearcher(searchers);v


(2)还有个进行多条件搜索 and 与 or 的操作————

用 MultiFieldQueryParser

建议重新封装

MultiFieldQueryParser.Parser(p[],d[],f[],analyer)  成or 与 and操作合一

或者

BooleanQuery m_BooleanQuery = new BooleanQuery();

Query query = QueryParser.Parse(m_SearchText, "INSTRUMENT_NAME", analyzer);

Query query2 = QueryParser.Parse(m_SearchText2, "INSTRUMENT_NAME2", analyzer);

m_BooleanQuery.Add(query, true, false);

m_BooleanQuery.Add(query2, true, false);

(3)复合查询(多种查询条件的综合查询)

Query query=MultiFieldQueryParser.parse("索引”,new String[] 学习lucene - 风 - 杨超峰的博客{"title","content"},analyzer);

Searcher searcher=new IndexSearcher(indexFilePath);

Hits hits=searcher.search(query);

for (int i = 0; i < hits.length(); i++) 学习lucene - 风 - 杨超峰的博客{

             System.out.println(hits.doc(i).get("name"));

}

5.为查询优化索引(index)

Indexwriter.optimize()方法可以为查询优化索引(index),之前提到的参数调优是为indexing过程本身优化,而这里是为查询优化,优化主要是减少index文件数,这样让查询的时候少打开文件,优化过程中,lucene会拷贝旧的index再合并,合并完成以后删除旧的index,所以在此期间,磁盘占用增加, IO符合也会增加,在优化完成瞬间,磁盘占用会是优化前的2倍,在optimize过程中可以同时作search。

4.org.apache.lucene.document.Field

        即上文所说的“字段”,它是Document的片段section。

         Field的构造函数:

        Field(String name, String string, boolean store, boolean index, boolean token)。

         Indexed:如果字段是Indexed的,表示这个字段是可检索的。

         Stored:如果字段是Stored的,表示这个字段的值可以从检索结果中得到。

         Tokenized:如果一个字段是Tokenized的,表示它是有经过Analyzer转变后成为一个tokens序列,在这个转变过程tokenization中, Analyzer提取出需要进行索引的文本,而剔除一些冗余的词句(例如:a,the,they等,详见 org.apache.lucene.analysis.StopAnalyzer.ENGLISH_STOP_WORDS和 org.apache.lucene.analysis.standard.StandardAnalyzer(String[] stopWords)的API)。Token是索引时候的.

类型

Analyzed

Indexed

Stored

说明

Field.Keyword(String,String/Date)

N

Y

Y

这个Field用来储存会直接用来检索的比如(编号,姓名,日期等)

Field.UnIndexed(String,String)

N

N

Y

不会用来检索的信息,但是检索后需要显示的,比如,硬件序列号,文档的url地址

Field.UnStored(String,String)

Y

Y

N

大段文本内容,会用来检索,但是检索后不需要从index中取内容,可以根据url去load真实的内容

Field.Text(String,String)

Y

Y

Y


 

5.Lucene 的检索结果排序

    Lucene的排序主要是对org.apache.lucene.search.Sort的使用。Sort可以直接根据字段Field生成,也可以根据标准的SortField生成,但是作为Sort的字段,必须符合以下的条件:唯一值以及Indexed。可以对Integers, Floats, Strings三种类型排序。

        对整数型的ID检索结果排序只要进行以下的简单操作:

Sort sort = new Sort("id");

Hits hits = searcher.search(query, sort);

用户还可以根据自己定义更加复杂的排序,详细请参考

6.分词器(分析器)

Lucene使用分析器来处理被索引的文本。在将其存入索引之前,分析器用于将文本标记化、摘录有关的单词、丢弃共有的单词、处理派生词(把派生词还原到词根形式,意思是把bowling、bowler和bowls还原为bowl)和完成其它要做的处理。Lucene提供的通用分析器是:

         SimpleAnalyzer:用字符串标记一组单词并且转化为小写字母。

         StandardAnalyzer:用字符串标记一组单词,可识别缩写词、email地址、主机名称等等。并丢弃基于英语的stop words (a, an, the, to)等、处理派生词。

         ChineseAnalyzer.class,它是一个单字分析法,它把句子中的词全部分成一个一个的字符,以单个字为单位存储。

         CJKAnalyzer.class,它是双字分析法,它把中文以双字为单位拆分得到结果,从而建立词条。当然这些得到的双字词中会有很多不符合中文语义单位的双字被送进索引。

十、需要注意的问题:

1 .IndexWriter在添加新的document后,需要重新建立Index,则需要调用writer.optimize();方法

2. Lucene没有update索引的方法,需要删除后重新建立,参考remove方法

3 .用IndexReader删除Document后,需要重新用IndexWriter进行整理,否则无法在进行搜索(不知道是不是我设置问题)

4.Lucene先在内存中进行索引操作,并根据一定的批量进行文件的写入。这个批次的间隔越大,文件的写入次数越少,但占用内存会很多。反之占用内存少,但文件IO操作频繁,索引速度会很慢。在IndexWriter中有一个MERGE_FACTOR参数可以帮助你在构造索引器后根据应用环境的情况充分利用内存减少文件的操作。根据我的使用经验:缺省Indexer是每20条记录索引后写入一次,每将MERGE_FACTOR增加50倍,索引速度可以提高1倍左右。

5.并发操作Lucene

(1)所有只读操作都可以并发

(2)在index被修改期间,所有只读操作都可以并发

(3)对index修改操作不能并发,一个index只能被一个线程占用

(4)ndex的优化,合并,添加都是修改操作

(5)但需要注意的是,在创建搜索的时候用:

searcher = new IndexSearcher(IndexReader.open("E:\\lucene\\test4\\index"));

searcher.close();

这时候是不能关闭searcher的.

如果想让searcher能关闭,就不要用IndexReader了:

   searcher = new IndexSearcher("E:\\lucene\\test4\\index"); 

6.Locking机制

lucence内部使用文件来locking,默认的locking文件放在java.io.tmpdir,可以通过-Dorg.apache.lucene.lockDir=xxx指定新的dir,有write.lock commit.lock两个文件,lock文件用来防止并行操作index,如果并行操作, lucene会抛出异常,可以通过设置-DdisableLuceneLocks=true来禁止locking,这样做一般来说很危险,除非你有操作系统或者物理级别的只读保证,比如把index文件刻盘到CDROM上。

十一、2.0中新增特性

1.新增类: org.apache.lucene.index.IndexModifier ,它合并了    IndexWriter 和 IndexReader,好处是我们可以增加和删除文档的时候不同担心 synchronisation/locking 的问题了。

2.增加对 contrib/highlighter 的 NullFragmenter , 这对全文本加亮很有用。

3.增加了新类 MatchAllDocsQuery 用来匹配所有文档。

4.. 增加 ParallelReader,这个一种IndexReader 他合并多个单独的索引到一个单独的虚拟索引上。

5.增加 Hits.iterator() 方法和相应的 HitIterator 和 Hit 对象。

他提供了对 Hits对象标准的 java.util.Iterator 叠代操作。

每个iterator's next() 方法返回一个 Hit 对象。

6. 在 term vectors 中增加了位置和偏移信息。(Grant Ingersoll & Christoph)

7. 增加了一个新的 DateTools 。允许用户格式化日期到一种更可读的格式,以便于更好的适应索引。

8. 增加了对压缩字段存储的支持。

实例:

1.判断索引文件是否存在:

 public static boolean indexExist(String indexDir){

        return IndexReader.indexExists(indexDir);

    }

    private IndexWriter getWriter(String indexFilePath) throws Exception {

         boolean append=true;

         File file=new File(indexFilePath+File.separator+"segments");

         if(file.exists())

             append=false;

         return new IndexWriter(indexFilePath,analyzer,append);

     }

2.删除索引

public static void deleteIndex(Term aTerm, String indexDir) {

        List aList = new ArrayList();

        aList.add(aTerm);

        deleteIndex(aList, indexDir);

    }

    public static void deleteIndex(List terms, String indexDir) {

        if (null == terms) {

            return;

        }

        if(!indexExist(indexDir)) { return; }

        IndexReader reader = null;

        try {

            reader = IndexReader.open(indexDir);

            for (int i = 0; i < terms.size(); i++){

                Term aTerm = (Term) terms.get(i);

                if (null != aTerm){

                    reader.delete(aTerm);

                }

            }

        } catch (IOException e){

            LogMan.warn("Error in Delete Index", e);

        } finally {

            try{

                if (null != reader){

                    reader.close();

                }

            }catch (IOException e){

                LogMan.warn("Close reader Error");

            }

        }

    }

    

删除索引需要一个条件,类似数据库中的字段条件,例如删除一条新闻的代码如下:

  

 public static void deleteNewsInfoIndex(int nid){

         Term aTerm = new Term("nid", String.valueOf(nid));

         deleteIndex(aTerm,indexDir);

     }   

 

public class LuceneSearch {

public static void main(String[] args) throws Exception{

   LuceneSearch test = new LuceneSearch();

   Hits h = null;

   h = test.search("显示 ");

   test.printResult(h);  

   h = test.search("jy");

   test.printResult(h);

   h = test.search("djy");

   test.printResult(h);

   h = test.search("料不");

   test.printResult(h);

   h = test.search("人");

   test.printResult(h);

}

public LuceneSearch(){

   try{

    searcher = new IndexSearcher(IndexReader.open("E:\\lucene\\test4\\index"));

   }catch(Exception e){

    e.printStackTrace();

   }

}

//声明一个IndexSearcher对象

private IndexSearcher searcher = null;

//声明一个Query对象

private Query query = null;

ChineseAnalyzer analyzer = new ChineseAnalyzer();

Highlighter highlighter = null;

public final Hits search(String keyword){

   System.out.println("正在检索关键字:"+keyword);

   try{

    Date start = new Date();

   

    /***** 一个关键字,对一个字段进行查询 *****/

    QueryParser qp = new QueryParser("content",analyzer);

    query = qp.parse(keyword);

//    Hits hits = searcher.search(query);

   

    /***** 模糊查询 *****/

//    Term term = new Term("content",keyword);

//    FuzzyQuery fq = new FuzzyQuery(term);

//    Hits hits = searcher.search(fq);

   

   

   

    /***** 一个关键字,在两个字段中查询 *****/

    /*

    * 1.BooleanClause.Occur[]的三种类型:

    *    MUST : + and

    *    MUST_NOT : - not

    *    SHOULD : or

    * 2.下面查询的意思是:content中必须包含该关键字,而title有没有都无所谓

    * 3.下面的这个查询中,Occur[]的长度必须和Fields[]的长度一致。每个限制条件对应一个字段

    */

//    BooleanClause.Occur[] flags = new BooleanClause.Occur[]{BooleanClause.Occur.SHOULD,BooleanClause.Occur.MUST};

//    query=MultiFieldQueryParser.parse(keyword,new String[]{"title","content"},flags,analyzer);

   

   

    /***** 两个(多个)关键字对两个(多个)字段进行查询,默认匹配规则 *****/

    /*

    * 1.关键字的个数必须和字段的个数相等

    * 2.由于没有指定匹配规定,默认为"SHOULD"

    *   因此,下面查询的意思是:"title"中含有keyword1 或 "content"含有keyword2.

    *   在此例中,把keyword1和keyword2相同

    */

//    query=MultiFieldQueryParser.parse(new String[]{keyword,keyword},new String[]{"title","content"},analyzer);

    /***** 两个(多个)关键字对两个(多个)字段进行查询,手工指定匹配规则 *****/

    /*

    * 1.必须 关键字的个数 == 字段名的个数 == 匹配规则的个数

    * 2.下面查询的意思是:"title"必须不含有keyword1,并且"content"中必须含有keyword2

    */

//    BooleanClause.Occur[] flags = new BooleanClause.Occur[]{BooleanClause.Occur.MUST_NOT,BooleanClause.Occur.MUST};

//    query=MultiFieldQueryParser.parse(new String[]{keyword,keyword},new String[]{"title","content"},flags,analyzer);

   

    /***** 对日期型字段进行查询 *****/

   

    /***** 对数字范围进行查询 *****/

    /*

    * 1.两个条件必须是同一个字段

    * 2.前面一个条件必须比后面一个条件小,否则找不到数据

    * 3.new RangeQuery中的第三个参数,表示是否包含"="

    *   true: >= 或 <=

    *   false: > 或 <

    * 4.找出 55>=id>=53 or 60>=id>=57:

    */

//    Term lowerTerm1 = new Term("id","53");

//    Term upperTerm1 = new Term("id","55");

//    RangeQuery rq1 = new RangeQuery(lowerTerm1,upperTerm1,true);

//   

//    Term lowerTerm2 = new Term("id","57");

//    Term upperTerm2 = new Term("id","60");

//    RangeQuery rq2 = new RangeQuery(lowerTerm2,upperTerm2,true);

//   

//    BooleanQuery bq = new BooleanQuery();

//    bq.add(rq1,BooleanClause.Occur.SHOULD);

//   bq.add(rq2,BooleanClause.Occur.SHOULD);    

//手工拼范围

//   query = QueryParser.Parse("{200004 TO 200206}", "pubmonth", new SimpleAnalyzer());

// Lucene用[] 和{}分别表示包含和不包含.

   //String temp = "startDate:["+nextWeek[0]+" TO "+nextWeek[1]+"] ";

   //   temp = temp + " OR endDate:["+nextWeek[0]+" TO "+nextWeek[1]+"]";

    // Query query1 = qp.parse(temp);

   

//   Hits hits = searcher.search(bq);

    /***** 排序 *****/

    /*

    * 1.被排序的字段必须被索引过(Indexecd),在索引时不能 用 Field.Index.TOKENIZED

    *   (用UN_TOKENIZED可以正常实现.用NO时查询正常,但排序不能正常设置升降序)

    * 2.SortField类型

    *   SCORE、DOC、AUTO、STRING、INT、FLOAT、CUSTOM

    *   此类型主要是根据字段的类型选择

    * 3.SortField的第三个参数代表是否是降序

    *   true:降序 false:升序

    */

//    Sort sort = new Sort(new SortField[]{new SortField("id", SortField.INT, true)});

//    Hits hits = searcher.search(query,sort);

   

    /*

    * 按日期排序

    */

   // Sort sort = new Sort(new SortField[]{new SortField("createTime", SortField.INT, false)});

  

   /***** 过滤器 ******/

//   QueryParser qp1 = new QueryParser("content",analyzer);

//   Query fquery = qp1.parse("我");

//  

//   BooleanQuery bqf = new BooleanQuery();

//    bqf.add(fquery,BooleanClause.Occur.SHOULD);

//  

//    QueryFilter qf = new QueryFilter(bqf);

   

   Hits hits = searcher.search(query);  

   

   

    Date end = new Date();

    System.out.println("检索完成,用时"+(end.getTime()-start.getTime())+"毫秒");

    return hits;

   }catch(Exception e){

    e.printStackTrace();

    return null;

   }

  

}

public void printResult(Hits h){

   if(h.length() == 0){

    System.out.println("对不起,没有找到您要的结果.");

   }else{

    for(int i = 0; i < h.length(); i++){

     try{

      Document doc = h.doc(i);

    

      System.out.println("结果"+(i+1)+":"+doc.get("id")+" createTime:"+doc.get("createTime")+" title:"+doc.get("title")+"        content:"+doc.get("content"));

    

      //System.out.println(doc.get("path"));

     }catch(Exception e){

      e.printStackTrace();

     }

    }

   }

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

}

}


  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值