Justificación de que Matcher arroje IllegalStateException cuando no se llama ningún método de “coincidencia”

TL; DR

¿Cuáles son las decisiones de diseño detrás de la API de Matcher ?

Fondo

Matcher tiene un comportamiento que no esperaba y para el cual no puedo encontrar una buena razón. La documentación API dice:

Una vez creado, un matcher se puede usar para realizar tres tipos diferentes de operaciones de coincidencia: […] Cada uno de estos métodos arroja un valor booleano que indica éxito o falla. Se puede obtener más información sobre una coincidencia exitosa al consultar el estado del emparejador.

Lo que dice la documentación de la API es:

El estado explícito de un emparejador no está definido inicialmente; intentar consultar cualquier parte del mismo antes de una coincidencia exitosa hará que se genere una IllegalStateException.

Ejemplo

 String s = "foo=23,bar=42"; Pattern p = Pattern.compile("foo=(?[0-9]*),bar=(?[0-9]*)"); Matcher matcher = p.matcher(s); System.out.println(matcher.group("foo")); // (1) System.out.println(matcher.group("bar")); 

Este código arroja un

 java.lang.IllegalStateException: No match found 

en (1) . Para evitar esto, es necesario llamar a matches() u otros métodos que lleven al Matcher a un estado que permita group() . Los siguientes trabajos:

 String s = "foo=23,bar=42"; Pattern p = Pattern.compile("foo=(?[0-9]*),bar=(?[0-9]*)"); Matcher matcher = p.matcher(s); matcher.matches(); // (2) System.out.println(matcher.group("foo")); System.out.println(matcher.group("bar")); 

Al agregar la llamada a matches() en (2) establece al Matcher en el estado adecuado para llamar al group() .

Pregunta, probablemente no constructiva

¿Por qué esta API está diseñada así? ¿Por qué no coincide automáticamente cuando el Matcher se Patter.matcher(String) con Patter.matcher(String) ?

En realidad, entendiste mal la documentación. Eche un segundo vistazo a la statement que citó:

intentar consultar cualquier parte del mismo antes de una coincidencia exitosa hará que se genere una IllegalStateException.

Un emparejador puede lanzar IllegalStateException al acceder a matcher.group() si no se encontró ninguna coincidencia.

Por lo tanto, debe usar la siguiente prueba para iniciar realmente el proceso de coincidencia:

  - matcher.matches() //Or - matcher.find() 

El siguiente código: –

 Matcher matcher = pattern.matcher(); 

Solo crea una instancia de matcher . Esto no coincidirá con una cadena. Incluso si hubo una coincidencia exitosa. Por lo tanto, debe verificar la siguiente condición para verificar si hay coincidencias exitosas:

 if (matcher.matches()) { // Then use `matcher.group()` } 

Y si la condición en el if devuelve false , eso significa que no se hizo coincidir nada. Por lo tanto, si usa matcher.group() sin verificar esta condición, obtendrá IllegalStateException si no se encontró la coincidencia.


Supongamos que, si Matcher fue diseñado de la manera en que usted está diciendo, entonces tendría que hacer una comprobación null para verificar si se encontró o no una coincidencia, para llamar a matcher.group() , así:

La forma en que crees que debería haberse hecho: –

 // Suppose this returned the matched string Matcher matcher = pattern.matcher(s); // Need to check whether there was actually a match if (matcher != null) { // Prints only the first match System.out.println(matcher.group()); } 

Pero, ¿qué pasa si desea imprimir más coincidencias, ya que un patrón puede coincidir varias veces en una cadena, para eso, debe haber una manera de decirle al emparejador que busque la siguiente coincidencia. Pero el cheque null no podría hacer eso. Para eso, tendrías que mover tu emparejador hacia adelante para que coincida con la siguiente Cadena. Entonces, hay varios métodos definidos en la clase de Matcher para cumplir con el propósito. El método matcher.find() coincide con la cadena hasta que se encuentren todas las coincidencias.

También hay otros métodos, que match la cadena de una manera diferente, que depende de usted cómo quiere coincidir. Por lo tanto, es en última instancia en la clase Matcher para hacer la matching contra la cadena. Pattern clase de Pattern solo crea un pattern para hacer coincidir. Si Pattern.matcher() match el patrón, entonces tiene que haber alguna forma de definir varias maneras de hacer match , ya que la matching puede ser de diferentes maneras. Entonces, surge la necesidad de la clase Matcher .

Entonces, la forma en que realmente es:

 Matcher matcher = pattern.matcher(s); // Finds all the matches until found by moving the `matcher` forward while(matcher.find()) { System.out.println(matcher.group()); } 

Por lo tanto, si se encuentran 4 coincidencias en la cuerda, la primera manera es que imprima solo la primera, mientras que la 2ª forma imprimirá todas las coincidencias, moviendo la matcher hacia adelante para que coincida con el siguiente patrón.

Espero que eso lo aclare.

La documentación de la clase de Matcher describe el uso de los tres métodos que proporciona, que dice:

Un emparejador se crea a partir de un patrón invocando el método de emparejamiento del patrón. Una vez creado, un matcher se puede usar para realizar tres tipos diferentes de operaciones de coincidencia:

  • El método de coincidencias intenta hacer coincidir toda la secuencia de entrada con el patrón.

  • El método lookingAt intenta hacer coincidir la secuencia de entrada, empezando por el principio, con el patrón.

  • El método find escanea la secuencia de entrada buscando la subsecuencia siguiente que coincida con el patrón.

Desafortunadamente, no he podido encontrar ninguna otra fuente oficial, diciendo explícitamente por qué y cómo este problema.

Mi respuesta es muy similar a la de Rohit Jain, pero incluye algunas razones por las cuales el paso ‘extra’ es necesario.

Implementación java.util.regex

La línea:

 Pattern p = Pattern.compile("foo=(?[0-9]*),bar=(?[0-9]*)"); 

hace que se asigne un nuevo objeto Pattern, y almacena internamente una estructura que representa la información RE, como una selección de caracteres, grupos, secuencias, codiciosos vs. no codiciosos, repeticiones, etc.

Este patrón es sin estado e inmutable, por lo que puede reutilizarse, es multi-lectura y se optimiza bien.

Las líneas:

 String s = "foo=23,bar=42"; Matcher matcher = p.matcher(s); 

devuelve un nuevo objeto Matcher para el Pattern y la String , uno que aún no ha leído la Cadena. Matcher es realmente solo el estado de una máquina de estado, donde la máquina de estado es el Pattern .

La coincidencia puede ejecutarse al avanzar la máquina de estado a través del proceso de coincidencia utilizando la siguiente API:

  • lookingAt() : intenta hacer coincidir la secuencia de entrada, empezando por el principio, con el patrón
  • find() : analiza la secuencia de entrada buscando la subsecuencia siguiente que coincida con el patrón.

En ambos casos, el estado intermedio se puede leer utilizando los métodos start() , end() y group() .

Beneficios de este enfoque

¿Por qué alguien querría pasar por el análisis?

  1. Obtenga valores de grupos que tienen una cuantificación mayor que 1 (es decir, grupos que repiten y terminan haciendo coincidir más de una vez). Por ejemplo, en el RE trivial a continuación que analiza las asignaciones de variables:

     Pattern p = new Pattern("([az]=([0-9]+);)+"); Matcher m = p.matcher("a=1;b=2;x=3;"); m.matches(); System.out.println(m.group(2)); // Only matches value for x ('3') - not the other values 

    Consulte la sección sobre “Nombre del grupo” en “Grupos y captura” el JavaDoc en el patrón

  2. El desarrollador puede usar el RE como un lexer y el desarrollador puede vincular los tokens lexed a un analizador . En la práctica, esto funcionaría para lenguajes de dominio simples, pero las expresiones regulares probablemente no sean el camino a seguir para obtener un lenguaje informático completo. EDITAR Esto se relaciona en parte con el motivo anterior, pero con frecuencia puede ser más fácil y más eficiente crear el árbol de análisis sintáctico que procesa el texto que leer primero todas las entradas.
  3. (Para los valientes) puede depurar RE y descubrir qué subsecuencia no coincide (o hacer coincidir incorrectamente).

Sin embargo, en la mayoría de las ocasiones no es necesario pasar la máquina de estado a través de la coincidencia, por lo que hay un método de conveniencia ( matches ) que ejecuta la coincidencia de patrones hasta la finalización.

Si un emparejador coincida automáticamente con la cadena de entrada, se desperdiciará esfuerzo en caso de que desee encontrar el patrón.

Se puede usar un matcher para verificar si el patrón matches() la cadena de entrada, y se puede usar para find() el patrón en la cadena de entrada (incluso repetidamente para encontrar todas las subcadenas coincidentes). Hasta que llame a uno de estos dos métodos, el emparejador no sabe qué prueba desea realizar, por lo que no le puede dar ningún grupo coincidente. Incluso si llama a uno de estos métodos, la llamada puede fallar, no se encuentra el patrón, y en ese caso una llamada al group debe fallar.

Esto es esperado y documentado.

La razón es que .matches() devuelve un valor booleano que indica si hubo una coincidencia. Si hubo una coincidencia, entonces puedes llamar .group(...) significativamente. De lo contrario, si no hay coincidencia, una llamada a .group(...) no tiene sentido. Por lo tanto, no debe permitirse llamar a .group(...) antes de llamar a matches() .

La forma correcta de usar un matcher es algo como lo siguiente:

 Matcher m = p.matcher(s); if (m.matches()) { ...println(matcher.group("foo")); ... } 

Supongo que la decisión de diseño se basó en consultas que tenían una semántica clara y bien definida que no combinaba la existencia con las propiedades de coincidencia.

Considere lo siguiente: ¿qué esperaría que vuelvan las consultas de Matcher si el emparejador no ha coincidido con éxito?

Consideremos primero el group() . Si no hemos logrado coincidir con algo, Matcher no debe devolver la cadena vacía, ya que no ha coincidido con la cadena vacía. Podríamos devolver null en este punto.

Ok, ahora consideremos start() y end() . Cada retorno int . ¿Qué valor int sería válido en este caso? Ciertamente no hay número positivo. ¿Qué número negativo sería apropiado? -1?

Dado todo esto, un usuario aún tendrá que verificar los valores devueltos para cada consulta para verificar si se produjo una coincidencia o no. De forma alternativa, podría verificar si coincide con éxito directamente, y si tiene éxito, la semántica de la consulta tiene un significado bien definido. Si no, el usuario obtiene un comportamiento consistente sin importar el ángulo que se consulta.

IllegalStateException que reutilizar IllegalStateException puede no haber resultado en la mejor descripción de la condición de error. Pero si IllegalStateException nombre / subclase IllegalStateException a NoSuccessfulMatchException , uno debería poder apreciar cómo el diseño actual impone la coherencia de la consulta y alienta al usuario a usar consultas que tengan semántica que se sabe que está definida en el momento de formularla.

TL; DR : ¿Cuál es el valor de preguntar la causa específica de la muerte de un organismo vivo?

matcher.matches() verificar el valor de retorno de matcher.matches() . Volverá true cuando se encontró una coincidencia, de lo contrario es false .

 if (matcher.matches()) { System.out.println(matcher.group("foo")); System.out.println(matcher.group("bar")); } 

Si matcher.matches() no encuentra una coincidencia y llama a matcher.group(...) , igual obtendrá una IllegalStateException . Eso es exactamente lo que dice la documentación:

El estado explícito de un emparejador no está definido inicialmente; intentar consultar cualquier parte del mismo antes de una coincidencia exitosa hará que se genere una IllegalStateException.

Cuando matcher.match() devuelve false , no se ha encontrado una coincidencia exitosa y no tiene mucho sentido obtener información sobre la coincidencia llamando al group() .